IIS hosting .NET development website in a different folder to wwwroot

When setting up new web site from repo, if the source code is not under wwwroot, you may get errors when browsing to the site.  Or you may get 401 errors on static files.

To ensure websites can run, go to the root folder of your repos (alternatively just choose the source folder).

For example c:\users\dave\source\repos

Give following permissions to local PC IIS_IUSERS group:

  • read & execute
  • list folder contents
  • read

In IIS admin, create new site (creates new Application Pool by default).  Click on the new Site, and double click on Authentication icon : right-click on Anonymous Authentication and choose Edit; set the anonymous user identity to “Application pool identity”.

 

To allow browsing from other PCs on same network/workgroup:

 

VSTS Test Filter Criteria for SpecFlow tests

If your specflow scenarios have tags (for example @navigation) and you have a Visual Studio Test task in VSTS, then you can selectively run only tests with a certain tag by completing the Test Filter Criteria setting in the Visual Studio Test task:

For example:

TestCategory=navigation

will run only those tests with the “navigation” tag.

Simple C# console app to execute command against SQL server database

This example uses the following Nuget packages:
https://www.nuget.org/packages/CommandLineParser/2.3.0/
https://www.nuget.org/packages/Dapper/1.50.5/

 

class Program
    {
        public class Options
        {
           [Option('c', "ConnectionString", Required = true, HelpText = "The SQL Connection string")]
            public string ConnectionString { get; set; }

           [Option('i', "InputFile", Required = true, HelpText = "The file path containing SQL query")]
            public string InputFile { get; set; }
        }

        static void Main(string[] args)
        {
            var connectionString = string.Empty;
            var inputFile = string.Empty;

            Parser.Default.ParseArguments(args)
                   .WithParsed(o =>
                   {
                       connectionString = o.ConnectionString;
                       inputFile = o.InputFile;
                   });

            if ( string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(inputFile) )
            {
                Console.WriteLine("Usage #1: RunSqlCmd -c \"CONNECTION-STRING-HERE\" -i \"FILE-PATH-TO-SQL-QUERY\"");
                Console.WriteLine("Usage #2: RunSqlCmd --ConnectionString \"CONNECTION-STRING-HERE\" --InputFile \"FILE-PATH-TO-SQL-QUERY\"");
                return ;
            }

            // get query file contents
            string query = System.IO.File.ReadAllText(inputFile);

            // execute the query
            using (var connection = new SqlConnection(connectionString))
            {
                connection.ExecuteScalar(query);
            }

        }

Debugging/Tracing System.Net

Using Visual Studio 2017 create a Console app.

Add the following to the app.config file:

 <system.diagnostics>
    <trace autoflush="true" />
    <sources>
      <source name="System.Net" maxdatasize="1024">
        <listeners>
          <add name="MyTraceFile"/>
        </listeners>
      </source>      
    </sources>
    <sharedListeners>
      <add
        name="MyTraceFile"
        type="System.Diagnostics.TextWriterTraceListener"
        initializeData="System.Net.trace.log"/>
    </sharedListeners>
    <switches>
      <add name="System.Net" value="Verbose" />
      <add name="System.Net.Sockets" value="Verbose" />
    </switches>
  </system.diagnostics>

 In the console app, make use of WebRequest:

               var fileContents = Encoding.UTF8.GetBytes("this is a test");

                Console.Write("UsePassive? (y/n)");
                var passive = Console.ReadKey();

                var usePassive = passive.Key == ConsoleKey.Y;

                Console.WriteLine("");
                Console.WriteLine("Using " + (usePassive ?  " UsePassive == true" : " UsePassive == false"));

                var request = (FtpWebRequest)WebRequest.Create("ftp://speedtest.tele2.net/upload/joeupload.txt");
                request.Credentials = new NetworkCredential("anonymous", "anonymous");
                request.Method = WebRequestMethods.Ftp.UploadFile;
                request.UseBinary = true;
                request.UsePassive = usePassive;
                request.KeepAlive = true;
                request.ContentLength = fileContents.Length;

                Console.WriteLine("Calling GetRequestStream...");
                using (var requestStream = request.GetRequestStream())
                {
                    Console.WriteLine("Calling WriteAsync...");
                    requestStream.WriteAsync(fileContents, 0, fileContents.Length);
                }
                Console.WriteLine("Calling GetResponse...");
                using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
                {
                    Console.WriteLine($"Upload File Complete, status {response.StatusDescription}");
                }

Running the console app will create a log file called (in this case via config) System.Net.trace.log

Selenium ‘ExpectedConditions’ is obsolete

In C# project ExpectedConditions gives warning:
‘ExpectedConditions’ is obsolete: ‘The ExpectedConditions implementation in the .NET bindings is deprecated and will be removed in a future release. This portion of the code has been migrated to the DotNetSeleniumExtras repository on GitHub (https://github.com/DotNetSeleniumTools/DotNetSeleniumExtras)’

            WebDriverWait wait2 = new WebDriverWait(driver, new TimeSpan(0,0,10));
            wait2.Until(ExpectedConditions.ElementIsVisible(By.Id("password")));

Not tried the NuGet package but should work!

IdentityServer4 – AddSigningCredential using certificate stored in Azure Key Vault

This post shows how to amend IdentityServer4 configuration from using AddDeveloperSigningCredential to AddSigningCredential with an X509 certificate.

The certificate will be stored as a secret in an Azure key vault.

In the IdentityServer4 Quick Start tutorials (Quick Starts), developer signing credentials are used, which is fine for development but in production a certificate should be used – this is required if, for example, Service Fabric is used to host an IdentityServer instance.

This is example of using developer signing credentials (in Startup.cs):

public void ConfigureServices(IServiceCollection services)
{
    // configure identity server with in-memory stores, keys, clients and resources
    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients());
}

With thanks to the following links:
Use Azure Key Vault from a Web Application
PFX Certificate in Azure Key Vault, by Rahul Nath
Use Azure Key Vault from a Web Application
Get started with Azure Key Vault

Step 1 – Create the certificate

A certificate can be created using OpenSSL (link).

First create the .key and .cer files:

openssl req –newkey rsa:2048 –nodes –keyout XXXXX.key –x509 –days 365 –out XXXXX.cer

Next, from these files create the .pfx:

openssl pkcs12 –export –in XXXX.cer –inkey XXXX.key –out XXXX.pfx

You will be asked to enter a password – note this down for later.

Step 2 – Upload the certificate into an Azure Key Vault as a secret

Open a Windows Powershell prompt as administrator.

Connect to your Azure account:

Connect-AzureRmAccount 

If necessary, change subscription:

Select-AzureRmSubscription -SubscriptionId AAAA-BBBB-CCCC-DDDDD123456 

Enter the following commands to import the certificate as a secret into your Key Vault:

$pfxFilePath = 'C:\XXXXXX.pfx'
$pwd = 'yourpassword'
$flag = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
$collection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$collection.Import($pfxFilePath, $pwd, $flag)
$pkcs12ContentType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12
$clearBytes = $collection.Export($pkcs12ContentType)
$fileContentEncoded = [System.Convert]::ToBase64String($clearBytes)
$secret = ConvertTo-SecureString -String $fileContentEncoded -AsPlainText –Force
$secretContentType = 'application/x-pkcs12'
Set-AzureKeyVaultSecret -VaultName 'YourKeyVaultName' -Name 'YourKeyName' -SecretValue $Secret -ContentType $secretContentType

Note that the password is required here to import the pfx into $collection – it is not stored in the Key Vault.

Login in to the Azure portal and navigate to your Key Vault. The secret should show in the “Secrets” tab. Click on your secret and in the properties tab take a copy of the “Secret Identifier” – eg. “https://yourvault.vault.azure.net/secrets/xyz/123&#8221;

Step 3 Create App Registration in Azure.

You need to create an “application registration” and assign this to have permissions to access your Azure key vault.

In the Azure portal, click on “All Services” in the left hand menu, then in the filter type “App registrations”.

Create application registration – ensure you choose “Web app/API” as the Application Type. The Application Id here will be the ClientId.

Click on the Application registration, then Settings, then Keys – enter a description for the key and set the expiration then click “Save” – copy the key value and make a note of it (as you will need it later & will not be able to retrieve it). The value you copy here will be the ClientSecret.

Step 4 Set the Access Policy the secret to your App Registration

In the Azure portal, navigate to Key Vault. Click on the Access Polices tab in Settings. Click “Add New”, then select your Application Registration from the “Select Principal” …
Under Key permissions, check the boxes “Decrypt” and “Sign” in the Cryptographic Operations section.
Under Secret Permissions, check the box “Get” in Secret Management Operations section.

Step 5 Identity Server web site.

See this link
To enable the IdentityServer to access the Azure key vault:
First, add the following NuGet packages:

  • Microsoft.IdentityModel.Clients.ActiveDirectory
  • icrosoft.Azure.KeyVault

Now modify the ConfigureServices method of Startup.cs to call AddSigningCredential (where GetIdentityServerCertificate is a new method that returns the certificate from the key vault)

 public void ConfigureServices(IServiceCollection services)
        {
           // (other code excluded) ...

            services.AddMvc();

            services.AddIdentityServer()
                .AddSigningCredential(GetIdentityServerCertificate(services))
                .AddInMemoryPersistedGrants()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(GetClientsFromConfig(services))
                .AddAspNetIdentity()
                .AddProfileService();
         }

 
Here is the implementation of GetIdentityServerCertificate

private X509Certificate2 GetIdentityServerCertificate(IServiceCollection services)
        {
             var clientId = "TODO-get from config";
            var clientSecret = "TODO-get from config";
            var secretIdentifier = "TODO-get from config";

            var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(async (authority, resource, scope) =>
            {
                var authContext = new AuthenticationContext(authority);
                ClientCredential clientCreds = new ClientCredential(clientId, clientSecret);

                AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCreds);

                if (result == null)
                {
                    throw new InvalidOperationException("Failed to obtain the JWT token");
                }

                return result.AccessToken;
            }));

            var pfxSecret = keyVaultClient.GetSecretAsync(secretIdentifier).Result;
            var pfxBytes = Convert.FromBase64String(pfxSecret.Value);
            var certificate = new X509Certificate2(pfxBytes);
            return certificate;
        }