STS Keyset does not exist even after MMC permission granted - x509certificate2

My application creates virtual directories on the fly as well as application pool for the STS-enabled web applications that run in those virtual directories. The application pools run under the ApplicationPoolIdentity account (IIS APPPOOL\MyAppPool). And I've been trying to figure out ways of programmatically grant access to the installed certificate.
My first approach was to use a batch file executing WinHttpCertCfg. However this approach will only work on app pool accounts that have been "activated". By "activated", I mean I have browsed to the new application at least once. Until this happens - WinHttpCertCfg always returns the message "The handle is invalid".
The next approach I tried was based on a solution obtained from here. This solution works in the sense that when I browse the certificate in MMC and select "Manage Certificate Keys", the app pool accounts are listed. Even when I run WinHttpCertCfg to list the accounts with access - the new app pools are listed.
But after all that...I still get "keyset does not exist" when I browse the web application.
My focus now is on fixing the second approach. Here is my modofication to the original code
public class CertificateHandler
{
private const string CommonApplicationKeys = #"Microsoft\Crypto\RSA\MachineKeys";
private const string PersonalKeys = #"Microsoft\Crypto\RSA\";
private static X509Certificate2 _personalCertificate = null;
private static X509Certificate2 _trustedCertificate = null;
public CertificateHandler(string thumbPrint)
{
X509Store personalStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
X509Store trustedStore = new X509Store(StoreName.TrustedPeople, StoreLocation.LocalMachine);
//open the stores to locate the certificates and cache for future use
if (_personalCertificate == null)
{
_personalCertificate = LoadCertificateFromStore(thumbPrint, personalStore);
}
if (_trustedCertificate == null)
{
_trustedCertificate = LoadCertificateFromStore(thumbPrint, trustedStore);
}
}
/// <summary>
/// Grants access to the specified certificate.
/// </summary>
/// <param name="thumbPrint">The thumb print of the certificate.</param>
/// <param name="user">The domain qualified user.</param>
public void GrantAccessToCertificate(string user)
{
//open the store to locate the certificate
GrantAccessToCertificate(user, _personalCertificate);
GrantAccessToCertificate(user, _trustedCertificate);
}
/// <summary>
/// Grants access to the specified certificate.
/// </summary>
/// <param name="user">The domain qualified user.</param>
/// <param name="certificate">The certificate to which access is granted</param>
private void GrantAccessToCertificate(string user, X509Certificate2 certificate)
{
RSACryptoServiceProvider crypto = certificate.PrivateKey as RSACryptoServiceProvider;
if (crypto != null)
{
//determine the location of the key
string keyfilepath = FindKeyLocation(crypto.CspKeyContainerInfo.UniqueKeyContainerName);
//obtain a file handle on the certificate
FileInfo file = new FileInfo(Path.Combine(keyfilepath, crypto.CspKeyContainerInfo.UniqueKeyContainerName));
//Add the user to the access control list for the certificate
FileSecurity fileControl = file.GetAccessControl();
NTAccount account = new NTAccount(user);
fileControl.AddAccessRule(new FileSystemAccessRule(account, FileSystemRights.FullControl, AccessControlType.Allow));
file.SetAccessControl(fileControl);
}
}
/// <summary>
/// Loads the certificate mathing the thumbprint from the specified store.
/// </summary>
/// <param name="thumbPrint">The thumb print of the certificate.</param>
/// <param name="store">The store.</param>
private X509Certificate2 LoadCertificateFromStore(string thumbPrint, X509Store store)
{
X509Certificate2 cert = null;
try
{
//fetch the certificates in the store
store.Open(OpenFlags.MaxAllowed);
//locate by the specified thumbprint
var results = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true);
if (results.Count > 0)
{
cert = results[0];
}
else
{
throw new FileNotFoundException("No certificate was found matching the specified thumbprint");
}
}
finally
{
store.Close();
}
return cert;
}
/// <summary>
/// Finds the key location.
/// </summary>
/// <param name="keyFileName">Name of the key file.</param>
/// <returns></returns>
private string FindKeyLocation(string keyFileName)
{
string location = string.Empty;
//start the search from the common application folder
string root = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
string commonLocation = Path.Combine(root, CommonApplicationKeys);
//filter for the key name
var keys = Directory.GetFiles(commonLocation, keyFileName);
if (keys.Length > 0)
{
location = commonLocation;
}
else
{
//now try the personal application folder
root = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string privateLocation = Path.Combine(root, PersonalKeys);
var subFolders = Directory.GetDirectories(privateLocation);
if (subFolders.Length > 0)
{
foreach (string folder in subFolders)
{
//filter for the key
keys = Directory.GetFiles(folder, keyFileName);
if (keys.Length != 0)
{
location = folder;
}
}
}
else
{
throw new InvalidOperationException("Private key exists but is not accessible");
}
}
return location;
}
}

I can now confirm that the code above actually works. The reason it appeared it wasn't working was that there was another app pool account to which I hadn't granted access to the certificate. Once that was done, it was all rosy from there on.

Related

How can I allow multiple domains in a .Net Web API with OAuth token authentication using CORS?

We have a .Net Framework Web API, with Token based OAuth authentication, and are trying to make a call to it via an Exchange HTML Add-In. I wish to allow access to several domains, as we may be using several different apps to access it, but we do not wish to allow general (*) access, as it is a proprietary web API, so there is no need for it to be accessed beyond known domains.
I have tried the following in order to satisfy the pre-flight:
Add the Access-Control-Allow-Origin headers with multiple domains via <system.webServer> - this returns a "header contains multiple values" CORS error when including multiple domains
Adding the Access-Control-Allow-Origin headers with multiple domains via a PreflightRequestsHandler : Delegating Handler - same result
If I set these up with one domain, and used the config.EnableCors with an EnableCorsAttribute with the domains, it would add those on to the headers and give an error with redundant domains.
How can I set up my Web API with OAuth and CORS settings for multiple domains?
You can add the header "Access-Control-Allow-Origin" in the response
of authorized sites in Global.asax file
using System.Linq;
private readonly string[] authorizedSites = new string[]
{
"https://site1.com",
"https://site2.com"
};
private void SetAccessControlAllowOrigin()
{
string origin = HttpContext.Current.Request.Headers.Get("Origin");
if (authorizedSites.Contains(origin))
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", origin);
}
protected void Application_BeginRequest()
{
SetAccessControlAllowOrigin();
}
Found the following from Oscar Garcia (#ozkary) at https://www.ozkary.com/2016/04/web-api-owin-cors-handling-no-access.html, implemented it and it worked perfectly! Added to AppOAuthProvider which Microsoft had set up on project creation:
/// <summary>
/// match endpoint is called before Validate Client Authentication. we need
/// to allow the clients based on domain to enable requests
/// the header
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task MatchEndpoint(OAuthMatchEndpointContext context)
{
SetCORSPolicy(context.OwinContext);
if (context.Request.Method == "OPTIONS")
{
context.RequestCompleted();
return Task.FromResult(0);
}
return base.MatchEndpoint(context);
}
/// <summary>
/// add the allow-origin header only if the origin domain is found on the
/// allowedOrigin list
/// </summary>
/// <param name="context"></param>
private void SetCORSPolicy(IOwinContext context)
{
string allowedUrls = ConfigurationManager.AppSettings["allowedOrigins"];
if (!String.IsNullOrWhiteSpace(allowedUrls))
{
var list = allowedUrls.Split(',');
if (list.Length > 0)
{
string origin = context.Request.Headers.Get("Origin");
var found = list.Where(item => item == origin).Any();
if (found){
context.Response.Headers.Add("Access-Control-Allow-Origin",
new string[] { origin });
}
}
}
context.Response.Headers.Add("Access-Control-Allow-Headers",
new string[] {"Authorization", "Content-Type" });
context.Response.Headers.Add("Access-Control-Allow-Methods",
new string[] {"OPTIONS", "POST" });
}

AssetManager vs Internal Storage: How can I store cross-platform config.json file in Xamarin Android that I want to edit later?

I want to be able to pre-configure my Android app to include a json file that is read/writeable, that is usable on other platforms as well (iOS, UWP). I started looking at AssetManager but found that I could only read the file, not edit it later.
Is internal storage the answer? If so, how do I pre-populate the app's internal storage with this file? I couldn't find any documentation online on how to do this besides performing a full write method in C# code, which kind of defeats the purpose of sharing the same config.json file on all platforms. I just want my Android app to store this config.json file, and have the file be readable and writeable.
Any help would be greatly appreciated.
I decided to answer my own question and show how it can be done in code. In this answer, I am using both an Android asset and internal storage to manipulate the file that contains my serialized object.
1) Copy your file into the Xamarin.Android project's Assets folder, and change its Build Action to "AndroidAsset". This will ensure that the file is packaged with your Android app initially.
2) In your C# code, what you really want is to read and write this file from your Android App's internal storage, not from the AssetManager (you can't write to the AssetManager anyways, only read!). So to do this, you must first pull the file from the AssetManager and then all subsequent read/writes will occur from internal storage.
/// <summary>
/// This method will read the application settings from internal storage.
/// If it can't find it, it will use the default AndroidAsset version of
/// the appsettings.json file from this project's Asset folder.
/// </summary>
/// <returns></returns>
public async Task<DefaultSettings> GetDefaultSettingsAsync()
{
var path = Application.Context.FilesDir.Path;
if (File.Exists(Path.Combine(path, "appsettings.json")))
{
using (var sr = Application.Context.OpenFileInput("appsettings.json"))
{
using (var ms = new MemoryStream())
{
await sr.CopyToAsync(ms);
var memoryBytes = ms.ToArray();
var content = System.Text.Encoding.Default.GetString(memoryBytes);
var defaultSettings = JsonConvert.DeserializeObject<DefaultSettings>(content);
return defaultSettings;
}
}
}
else
{
var result = await GetDefaultSettingsAssetAsync();
return result;
}
}
/// <summary>
/// This method gets the appsettings.json file from the Android AssetManager, in the case
/// where you do not have an editable appsettings.json file yet in your Android app's internal storage.
/// </summary>
/// <returns></returns>
private static async Task<DefaultSettings> GetDefaultSettingsAssetAsync()
{
try
{
var assetsManager = MainActivity.Instance.Assets;
using (var sr = assetsManager.Open("appsettings.json"))
{
using (var ms = new MemoryStream())
{
await sr.CopyToAsync(ms);
var memoryBytes = ms.ToArray();
var content = System.Text.Encoding.Default.GetString(memoryBytes);
var defaultSettings = JsonConvert.DeserializeObject<DefaultSettings>(content);
// This will write the Asset to Internal Storage for the first time. All subsequent writes will occur using SaveDefaultSettingsAsync.
using (var stream = Application.Context.OpenFileOutput("appsettings.json", FileCreationMode.Private))
{
await stream.WriteAsync(memoryBytes, 0, memoryBytes.Length);
}
return defaultSettings;
}
}
}
catch (Exception ex)
{
return new DefaultSettings();
}
}
/// <summary>
/// This method will save the DefaultSettings to your appsettings.json file in your Android App's internal storage.
/// </summary>
/// <param name="settings"></param>
/// <returns></returns>
public async Task SaveDefaultSettingsAsync(DefaultSettings settings)
{
using (var stream = Application.Context.OpenFileOutput("appsettings.json", FileCreationMode.Private))
{
var content = JsonConvert.SerializeObject(settings);
var bytes = Encoding.Default.GetBytes(content);
await stream.WriteAsync(bytes, 0, bytes.Length);
}
}

Microsoft Graph API - Manage Booking API Authorization has been denied for this request

I have a simple ASP.Net web application consist of .aspx web from hosted on azure as cloud service. In my application there is no user login.
I want to connect with Microsoft Graph API and and to use Microsoft Bookings API to get the BookingBusiness collection on my home page load without user login. I am currently debugging my web app on my desktop using Azure emulator.
I have the ofiice 365 premium account access assoiciated with my microsoft account (v-sheeal#microsoft.com) and I had created a Booking business using my v- alias through Booking tools (https://outlook.office.com/owa/?path=/bookings).
I registered an app in AAD in the same tenant with all required permission and provided the Cliend Id and secret in the code to get the access token. I am using Client credentials Grant flow to get the access token and try to invoke the booking API. I am able to get the access token, but when the code try to get the the list of booking businesses it is giving below exception.
DataServiceClientException: {
"error": {
"code": "",
"message": "Authorization has been denied for this request.",
"innerError": {
"request-id": "d0ac6470-9aae-4cc2-9bf3-ac83e700fd6a",
"date": "2018-09-03T08:38:29"
}
}
}
The code and registered app setting details are in below screen shot.
.aspx.cs
private static async Task<AuthenticationResult> AcquireToken()
{
var tenant = "microsoft.onmicrosoft.com";
//"yourtenant.onmicrosoft.com";
var resource = "https://graph.microsoft.com/";
var instance = "https://login.microsoftonline.com/";
var clientID = "7389d0b8-1611-4ef9-a01f-eba4c59a6427";
var secret = "mxbPBS10|[#!mangJHQF791";
var authority = $"{instance}{tenant}";
var authContext = new AuthenticationContext(authority);
var credentials = new ClientCredential(clientID, secret);
var authResult = await authContext.AcquireTokenAsync(resource,
credentials);
return authResult;
}
protected void MSBooking()
{
var authenticationContext = new
AuthenticationContext(GraphService.DefaultAadInstance,
TokenCache.DefaultShared);
var authenticationResult = AcquireToken().Result;
var graphService = new GraphService(
GraphService.ServiceRoot,
() => authenticationResult.CreateAuthorizationHeader());
// Get the list of booking businesses that the logged on user can see.
var bookingBusinesses = graphService.BookingBusinesses; ----- this
line throwing an exception "Authorization has been denied for
this request."
}
GraphService.cs
namespace Microsoft.Bookings.Client
{
using System;
using System.Net;
using Microsoft.OData;
using Microsoft.OData.Client;
public partial class GraphService
{
/// <summary>
/// The resource identifier for the Graph API.
/// </summary>
public const string ResourceId = "https://graph.microsoft.com/";
/// <summary>
/// The default AAD instance to use when authenticating.
/// </summary>
public const string DefaultAadInstance =
"https://login.microsoftonline.com/common/";
/// <summary>
/// The default v1 service root
/// </summary>
public static readonly Uri ServiceRoot = new
Uri("https://graph.microsoft.com/beta/");
/// <summary>
/// Initializes a new instance of the <see
cref="BookingsContainer"/> class.
/// </summary>
/// <param name="serviceRoot">The service root.</param>
/// <param name="getAuthenticationHeader">A delegate that returns
the authentication header to use in each request.</param>
public GraphService(Uri serviceRoot, Func<string>
getAuthenticationHeader)
: this(serviceRoot)
{
this.BuildingRequest += (s, e) => e.Headers.Add("Authorization",
getAuthenticationHeader());
}
}
According to your description, I assume you want to use the Microsoft Bookings API.
Base on the images you’ve provided, You are missing define scope in your code and the Authority is incorrectly.
We can review document to get an Access Token without a user.

What is the burden of User.Identity.GetUserId()?

In my ASP.NET MVC applications I use User.Identity.GetUserId() abundantly. However, I wonder if this has severe performance penalties.
Alternatively, I believe I can do this: In a View, I can assign the current user's id to a hidden field in the first page load. Then, when making AJAX calls, I can pass the hidden field value to controllers' actions. This way, I would not need to use User.Identity.GetUserId() method to retrieve the userid of the current user.
I wonder if anyone has any ideas on this?
Take a look at the source for GetUserId extension method:
/// <summary>
/// Return the user id using the UserIdClaimType
/// </summary>
/// <param name="identity"></param>
/// <returns></returns>
public static string GetUserId(this IIdentity identity)
{
if (identity == null)
{
throw new ArgumentNullException("identity");
}
var ci = identity as ClaimsIdentity;
if (ci != null)
{
return ci.FindFirstValue(ClaimTypes.NameIdentifier);
}
return null;
}
/// <summary>
/// Return the claim value for the first claim with the specified type if it exists, null otherwise
/// </summary>
/// <param name="identity"></param>
/// <param name="claimType"></param>
/// <returns></returns>
public static string FindFirstValue(this ClaimsIdentity identity, string claimType)
{
if (identity == null)
{
throw new ArgumentNullException("identity");
}
var claim = identity.FindFirst(claimType);
return claim != null ? claim.Value : null;
}
Every time you call that extension method it searches the identity for the ClaimTypes.NameIdentifier claim.
The performance impact is not that substantial (IMO) but leaking user information in hidden (there not actually hidden if one can see them with one click of view source) is not a good idea.
If you are concerned about calling it multiple times and need it in multiple locations through out a request then you can have it lazy loaded behind a property in your controller or a base controller.
private string userId
public string UserId {
get {
if(userid == null) {
userid = User.Identity.GetUserId();
}
return userid;
}
}
You could also create a service to encapsulate that information.

Get required fields for some WorkItem

Is it possible to get such XML, using TFS API or other tools?
This XML contains information about the fields that must be filled in translation work item to another status.
Screen here http://sqlrefactorstudio.com/content/png/TFS%20Work%20item%20required%20fields.png
Using the TFS API in the simple example below will write out the required fields for a given work item.
/// <summary>
/// Writes out the required fields for a work item.
/// </summary>
/// <param name="workItemId">The ID of a work item.</param>
private static void _GetRequiredFieldsForWorkItem(int workItemId)
{
using (TeamProjectPicker tpp = new TeamProjectPicker(TeamProjectPickerMode.SingleProject, false, new UICredentialsProvider()))
{
if (tpp.ShowDialog() == DialogResult.OK)
{
TfsTeamProjectCollection projectCollection = tpp.SelectedTeamProjectCollection;
WorkItemStore store = projectCollection.GetService<WorkItemStore>();
Console.WriteLine("Required Work Item Fields");
Console.WriteLine("-------------------------------");
WorkItem item = store.GetWorkItem(workItemId);
foreach (Field field in item.Fields)
{
if (field.IsRequired)
{
Console.WriteLine(field.ReferenceName);
}
}
}
}
}

Resources