Azure Key Vault Operation get is not allowed on a disabled secret - asp.net-core

We have implemented Azure key vault in the .NET core application. Everything is working fine until we disabled the secret from the list - After my application tries to fetch the list again it started giving me the exception
Unhandled exception. Microsoft.Azure.KeyVault.Models.KeyVaultErrorException: Operation get is not allowed on a disabled secret.
at Microsoft.Azure.KeyVault.KeyVaultClient.GetSecretWithHttpMessagesAsync(String vaultBaseUrl, String secretName, String secretVersion, Dictionary`2 customHeaders, CancellationToken cancellationToken)
at Microsoft.Azure.KeyVault.KeyVaultClientExtensions.GetSecretAsync(IKeyVaultClient operations, String secretIdentifier, CancellationToken cancellationToken)
at Microsoft.Extensions.Configuration.AzureKeyVault.AzureKeyVaultConfigurationProvider.LoadAsync()
at Microsoft.Extensions.Configuration.AzureKeyVault.AzureKeyVaultConfigurationProvider.Load()
at Microsoft.Extensions.Configuration.ConfigurationRoot..ctor(IList`1 providers)
at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
at Microsoft.Extensions.Hosting.HostBuilder.BuildAppConfiguration()
at Microsoft.Extensions.Hosting.HostBuilder.Build()
at Vodafone.LandingPage.Program.Main(String[] args) in D:\a\1\s\src\LandingPage\Program.cs:line 30
Code I use to connect with Key Vault in program.cs file.
if (ctx.HostingEnvironment.IsProduction())
{
var builtConfig = builder.Build();
var keyVaultEndpoint = $"https://{builtConfig["AppSettings:KeyVaultName"]}.vault.azure.net/";
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
builder.AddAzureKeyVault(keyVaultEndpoint, keyVaultClient, new DefaultKeyVaultSecretManager());
}
How we can restrict the list so that it will not take the disabled secrets together.
I am using "Get" and "List" permission.

After a research I found below solution.
You can use it like this
Problem : Code which read all secret
builder.AddAzureKeyVault(keyVaultEndpoint, keyVaultClient, new DefaultKeyVaultSecretManager());
Solution : Code Which read only enabled secrets
builder.AddAzureKeyVault(keyVaultEndpoint,keyVaultClient,new PrefixKeyVaultSecretManager(keyVaultEndpoint));
Implementation of IKeyVaultSecretManager
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
namespace KeyVaultPOC
{
public class PrefixKeyVaultSecretManager : IKeyVaultSecretManager
{
private readonly IList<string> _overrides = new List<string>();
public PrefixKeyVaultSecretManager(string vaultUrl)
{
Task.Run(() => LoadListOfOverrides(vaultUrl)).Wait();
}
private async Task LoadListOfOverrides(string vaultUrl)
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback)
);
var secrets = await keyVaultClient.GetSecretsAsync(vaultUrl);
bool moreSecrets;
do
{
foreach (var secret in secrets)
{
if ((bool)secret.Attributes.Enabled)
{
_overrides.Add(secret.Identifier.Name);
}
}
moreSecrets = !string.IsNullOrEmpty(secrets.NextPageLink);
if (moreSecrets)
{
secrets = await keyVaultClient.GetSecretsNextAsync(secrets.NextPageLink);
}
} while (moreSecrets);
}
public bool Load(SecretItem secret)
{
return true;
}
public string GetKey(SecretBundle secret)
{
var key = secret.SecretIdentifier.Name;
return key;
}
}
}
Ref : https://gist.github.com/davidxcheng/0576659d2c876d299619d979767dcdd6

Related

Are there advantages of using .NET Core's middleware "Health checks" over just a ping in a controller's route?

I'm reading a "Asp.net Core 3 and Angular 9" book and there is an example usage of .NET Core Health check.
It's also described on Microsoft website: https://learn.microsoft.com/en-US/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-3.0
I can't find a reason to actually use it over just creating a route in some controller which will ping external addresses.
Code in a book goes like this:
Add this in Configure (Startup.cs) method:
app.UseHealthChecks("/hc", new CustomHealthCheckOptions());
ConfigureServices method:
services.AddHealthChecks()
.AddCheck("ICMP_01", new ICMPHealthCheck("www.ryadel.com", 100))
.AddCheck("ICMP_02", new ICMPHealthCheck("www.google.com", 100))
.AddCheck("ICMP_03", new ICMPHealthCheck("www.does-notexist.com", 100));
Create ICMPHealthCheck.cs file:
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;
using System.Net.NetworkInformation;
using System.Threading;
using System.Threading.Tasks;
namespace HealthCheck
{
public class ICMPHealthCheck : IHealthCheck
{
private string Host { get; set; }
private int Timeout { get; set; }
public ICMPHealthCheck(string host, int timeout)
{
Host = host;
Timeout = timeout;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
using (var ping = new Ping())
{
var reply = await ping.SendPingAsync(Host);
switch (reply.Status)
{
case IPStatus.Success:
var msg = String.Format(
"IMCP to {0} took {1} ms.",
Host,
reply.RoundtripTime);
return (reply.RoundtripTime > Timeout)
? HealthCheckResult.Degraded(msg)
: HealthCheckResult.Healthy(msg);
default:
var err = String.Format(
"IMCP to {0} failed: {1}",
Host,
reply.Status);
return HealthCheckResult.Unhealthy(err);
}
}
}
catch (Exception e)
{
var err = String.Format(
"IMCP to {0} failed: {1}",
Host,
e.Message);
return HealthCheckResult.Unhealthy(err);
}
}
}
}
Create CustomHealthCheckOptions.cs file:
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
namespace HealthCheck
{
public class CustomHealthCheckOptions : HealthCheckOptions
{
public CustomHealthCheckOptions() : base()
{
var jsonSerializerOptions = new JsonSerializerOptions()
{
WriteIndented = true
};
ResponseWriter = async (c, r) =>
{
c.Response.ContentType =
MediaTypeNames.Application.Json;
c.Response.StatusCode = StatusCodes.Status200OK;
var result = JsonSerializer.Serialize(new
{
checks = r.Entries.Select(e => new
{
name = e.Key,
responseTime = e.Value.Duration.TotalMilliseconds,
status = e.Value.Status.ToString(),
description = e.Value.Description
}),
totalStatus = r.Status,
totalResponseTime =
r.TotalDuration.TotalMilliseconds,
}, jsonSerializerOptions);
await c.Response.WriteAsync(result);
};
}
}
}
So it just pings 3 addresses and I can't see advantages of using Microsoft.AspNetCore.Diagnostics.HealthChecks library. Is that wrong example?

ASP.Net Core - EC2 to S3 file upload with Access Denied

I have developed a .NET Core 3.1 Web API which allows the users to upload their documents to S3 bucket. When I deploy the API to AWS ElasticBeansTalk EC2 instance and call the endpoint which uploads the file to S3, I get an error "Access Denied".
By the way, I have created IAM policy and role to give full access to S3 from my EC2 instance. I have also copied the .aws folder which contains credentials file onto the EC2 instance.
API Controller Action
public async Task<ApiResponse> UpdateProfilePic([FromBody]UploadProfilePicRequest model)
{
using (Stream stream = model.profilePicData.Base64StringToStream(out string header))
{
var tags = new List<KeyValuePair<string, string>>();
var metaData = new List<KeyValuePair<string, string>>();
metaData.Add(new KeyValuePair<string, string>("Content-Disposition", $"attachment; filename=\"{model.filename}\""));
if (_host.IsDevelopment())
{
tags.Add(new KeyValuePair<string, string>("public", "yes"));
}
await AmazonS3Uploader.UploadFileAsync(stream, "myDir/", model.fileId, tags, metaData);
}
}
The AmazonS3Helper class shown below:
using Amazon;
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Transfer;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace UploderApp.Services
{
public static class AmazonS3Uploader
{
private static readonly RegionEndpoint bucketRegion = RegionEndpoint.APSouth1;
private static readonly IAmazonS3 s3Client = new AmazonS3Client(GetAwsCredentials(), bucketRegion);
private static readonly string S3Bucket = "abc-test";
private static AWSCredentials GetAwsCredentials()
{
var chain = new CredentialProfileStoreChain();
if (chain.TryGetAWSCredentials("MYPROFILE", out AWSCredentials awsCredentials))
{
return awsCredentials;
}
return null;
}
public static async Task UploadFileAsync(Stream fileStream, string virtualDirectory, string keyName)
{
try
{
using (var fileTransferUtility = new TransferUtility(s3Client))
{
//Upload data from a type of System.IO.Stream.
await fileTransferUtility.UploadAsync(fileStream, S3Bucket, virtualDirectory + keyName).ConfigureAwait(true);
}
}
catch (AmazonS3Exception e)
{
throw new Exception($"Error encountered on server. Message:'{e.Message}' when writing an object");
}
}
public static async Task UploadFileAsync(Stream stream, string virtualDirectory, string keyName, List<KeyValuePair<string, string>> tags = null, List<KeyValuePair<string, string>> metadata = null)
{
try
{
// Specify advanced settings.
var fileTransferUtilityRequest = new TransferUtilityUploadRequest
{
BucketName = S3Bucket,
InputStream = stream,
StorageClass = S3StorageClass.Standard,
Key = virtualDirectory + keyName
};
if (metadata != null)
{
foreach (var item in metadata)
{
fileTransferUtilityRequest.Metadata.Add(item.Key, item.Value);
}
}
if (tags != null)
{
fileTransferUtilityRequest.TagSet = new List<Tag>();
foreach (var tag in tags)
{
fileTransferUtilityRequest.TagSet.Add(new Tag { Key = tag.Key, Value = tag.Value });
}
}
using (var fileTransferUtility = new TransferUtility(s3Client))
{
await fileTransferUtility.UploadAsync(fileTransferUtilityRequest).ConfigureAwait(true);
}
}
catch (AmazonS3Exception e)
{
throw new Exception($"Error encountered on server. Message:'{e.Message}' when writing an object");
}
}
}
}
However, if I create a console application and use the above class without any modifications, it uploads the file from the same EC2 instance.
Code from the Main function of my Console Application.
public static void Main()
{
var file = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "/Screenshot.png";
try
{
var tags = new List<KeyValuePair<string, string>>();
var metaData = new List<KeyValuePair<string, string>>();
metaData.Add(new KeyValuePair<string, string>("Content-Disposition", $"attachment; filename=\"profile-pic.png\""));
using (var stream = new FileStream(file, FileMode.Open))
{
AmazonS3Uploader.UploadFileAsync(stream, "mydir/", "screenshot.png", tags, metaData).GetAwaiter().GetResult();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
This is very strange. Can anybody help me to understand the root cause, please?
Edit:1
Output of the aws s3 ls s3://abc-test is shown below
Edit:2
Uploading the EC2 folder to S3

IdentityServer 4 - Custom IExtensionGrantValidator always return invalid_grant

My app requirements is to authenticate using client credentials AND another code (hash).
I followed this link to create and use custom IExtensionGrantValidator.
I manged to invoke the custom IExtensionGrantValidator with approved grant, but client always gets invalid_grant error.
For some reason the set operation ofd Result (property of ExtensionGrantValidationContext) always fails (overriding the Error value returns the overrided value to client).
This is CustomGrantValidator Code:
public class CustomGrantValidator : IExtensionGrantValidator
{
public string GrantType => "grant-name";
public Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result
}
}
Startup.cs contains this line:
services.AddTransient<IExtensionGrantValidator, CustomGrantValidator>();
And finally client's code:
var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:5000") };
var disco = await httpClient.GetDiscoveryDocumentAsync("http://localhost:5000");
var cReq = await httpClient.RequestTokenAsync(new TokenRequest
{
GrantType = "grant-name",
Address = disco.TokenEndpoint,
ClientId = clientId,// client Id taken from appsetting.json
ClientSecret = clientSecret, //client secret taken from appsetting.json
Parameters = new Dictionary<string, string> { { "hash", hash } }
});
if (cReq.IsError)
//always getting 'invalid_grant' error
throw InvalidOperationException($"{cReq.Error}: {cReq.ErrorDescription}");
The below codes works on my environment :
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result;
return;
}
Don't forget to register the client to allow the custom grant :
return new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = { "grant-name" },
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
I got the same issue and found the answer from #Sarah Lissachell, turn out that I need to implement the IProfileService. This interface has a method called IsActiveAsync. If you don't implement this method, the answer of ValidateAsync will always be false.
public class IdentityProfileService : IProfileService
{
//This method comes second
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//IsActiveAsync turns out to be true
//Here you add the claims that you want in the access token
var claims = new List<Claim>();
claims.Add(new Claim("ThisIsNotAGoodClaim", "MyCrapClaim"));
context.IssuedClaims = claims;
}
//This method comes first
public async Task IsActiveAsync(IsActiveContext context)
{
bool isActive = false;
/*
Implement some code to determine that the user is actually active
and set isActive to true
*/
context.IsActive = isActive;
}
}
Then you have to add this implementation in your startup page.
public void ConfigureServices(IServiceCollection services)
{
// Some other code
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<Users>()
.AddInMemoryApiResources(config.GetApiResources())
.AddExtensionGrantValidator<CustomGrantValidator>()
.AddProfileService<IdentityProfileService>();
// More code
}

google oauth 2 authorization when using their indexing api

I'm trying to make sense of the google indexing api but their documentation is horrible. I've gone through setting up the service account and downloading the json file along with the remaining prerequisites. The next step is to get an access token to authenticate.
I'm in a .net environment but they don't give an example for that. I did find some example of using a .net library to do it here, but after the following code I'm not sure what service would be created to then make the call to the indexing api. I don't see a google.apis.indexing library in the nuget package manager.
UserCredential credential;
using (var stream = new FileStream("client_secrets.json", FileMode.Open, FileAccess.Read))
{
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
new[] { "https://www.googleapis.com/auth/indexing" },
"user", CancellationToken.None, new FileDataStore("IndexingStore"));
}
In their example code it looks like just a simple json post. I tried that but of course it doesn't work because I'm not authenticated. I'm just not sure how to tie all of this together in a .net environment.
You're right, Google's documentation for this is either not there or is terrible. Even their own docs have broken or unfinished pages in them and in one of them you're pointed to a nuget package that doesn't exist. It is possible to get this to work though by cobbling together other Auth examples on SA and then following the Java indexing documentation.
First, you'll need to use nuget package manager to add the main api package and the auth package:
Google.Apis
Google.Apis.Auth
Then try the following:
using System;
using System.Configuration;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Http;
using Newtonsoft.Json;
namespace MyProject.Common.GoogleForJobs
{
public class GoogleJobsClient
{
public async Task<HttpResponseMessage> AddOrUpdateJob(string jobUrl)
{
return await PostJobToGoogle(jobUrl, "URL_UPDATED");
}
public async Task<HttpResponseMessage> CloseJob(string jobUrl)
{
return await PostJobToGoogle(jobUrl, "URL_DELETED");
}
private static GoogleCredential GetGoogleCredential()
{
var path = ConfigurationManager.AppSettings["GoogleForJobsJsonFile"];
GoogleCredential credential;
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream)
.CreateScoped(new[] { "https://www.googleapis.com/auth/indexing" });
}
return credential;
}
private async Task<HttpResponseMessage> PostJobToGoogle(string jobUrl, string action)
{
var googleCredential = GetGoogleCredential();
var serviceAccountCredential = (ServiceAccountCredential) googleCredential.UnderlyingCredential;
const string googleApiUrl = "https://indexing.googleapis.com/v3/urlNotifications:publish";
var requestBody = new
{
url = jobUrl,
type = action
};
var httpClientHandler = new HttpClientHandler();
var configurableMessageHandler = new ConfigurableMessageHandler(httpClientHandler);
var configurableHttpClient = new ConfigurableHttpClient(configurableMessageHandler);
serviceAccountCredential.Initialize(configurableHttpClient);
HttpContent content = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json");
var response = await configurableHttpClient.PostAsync(new Uri(googleApiUrl), content);
return response;
}
}
}
You can then just call it like this
var googleJobsClient = new GoogleJobsClient();
var result = await googleJobsClient.AddOrUpdateJob(url_of_vacancy);
Or if you're not inside an async method
var googleJobsClient = new GoogleJobsClient();
var result = googleJobsClient.AddOrUpdateJob(url_of_vacancy).Result;

TwitterSettings.OAuthVersion

i don't understand that : GetRequestToken is not working in TweetSharp on Windows Phone
My code :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using PhoneApp2.Resources;
using TweetSharp;
namespace PhoneApp2
{
public partial class MainPage : PhoneApplicationPage
{
private const string consumerKey = "zvBvaKjEQRwGqu9ECaNfop0pr";
private const string consumerSecret = "SgEqsMRcIrEYNrtXhvtYdnx7qBA9EITzswneyjf8wRorDvSAvn";
private TwitterService myclient;
private OAuthRequestToken requestToken;
private bool userAuthenticated = false;
// Constructeur
public MainPage()
{
InitializeComponent();
myclient = new TwitterService(consumerKey, consumerSecret);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//If user is already logged in, just send the tweet, otherwise get the RequestToken
if (userAuthenticated)
//send the Tweet, this is just a placeholder, we will add the actual code later
Dispatcher.BeginInvoke(() => { MessageBox.Show("Placeholder for tweet sending"); });
else
myclient.GetRequestToken(processRequestToken);
}
private void processRequestToken(OAuthRequestToken token, TwitterResponse response)
{
if (token == null)
Dispatcher.BeginInvoke(() => { MessageBox.Show("Error getting request token"); });
else
{
requestToken = token;
Dispatcher.BeginInvoke(() =>
{
Browser.Visibility = System.Windows.Visibility.Visible;
Browser.Navigate(myclient.GetAuthorizationUri(requestToken));
});
}
}
}
}
and visual studio 2013 create an error on myclient.GetRequestToken(processRequestToken); ...
how can incorporate your solution with hammock on my code?
I had this identical error last week (doing this app) The solution was to implement the Hammock Library instead of tweet sharp. Also in the post tweet example change the version from 1 to 1.1
This is the Nokia Developer Documentation I followed to implement logging in
This is the Nokia Developer Documentation I followed to implement posting a tweet
REMEMBER CHANGE THE VERSION TO 1.1 LIKE THIS
From this
var credentials = new OAuthCredentials
{
Type = OAuthType.ProtectedResource,
SignatureMethod = OAuthSignatureMethod.HmacSha1,
ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
ConsumerKey = AppSettings.consumerKey,
ConsumerSecret = AppSettings.consumerKeySecret,
Token = this.accessToken,
TokenSecret = this.accessTokenSecret,
Version = "1.0"
};
var restClient = new RestClient
{
Authority = "http://api.twitter.com",
HasElevatedPermissions = true
};
var restRequest = new RestRequest
{
Credentials = credentials,
Path = "/1/statuses/update.json",
Method = WebMethod.Post
};
restRequest.AddParameter("status", txtTweetContent.Text);
restClient.BeginRequest(restRequest, new RestCallback(PostTweetRequestCallback));
To This
var credentials = new OAuthCredentials
{
Type = OAuthType.ProtectedResource,
SignatureMethod = OAuthSignatureMethod.HmacSha1,
ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
ConsumerKey = AppSettings.consumerKey,
ConsumerSecret = AppSettings.consumerKeySecret,
Token = this.accessToken,
TokenSecret = this.accessTokenSecret,
Version = "1.0"
};
var restClient = new RestClient
{
Authority = "http://api.twitter.com",
HasElevatedPermissions = true
};
var restRequest = new RestRequest
{
Credentials = credentials,
Path = "/1.1/statuses/update.json",
Method = WebMethod.Post
};
restRequest.AddParameter("status", txtTweetContent.Text);
restClient.BeginRequest(restRequest, new RestCallback(PostTweetRequestCallback));