Skip to main content

2 posts tagged with "authorisation"

View All Tags

Azure Easy Auth and Roles with .NET (and .NET Core)

If this post is interesting to you, you may also want to look at this one where we try to use Microsoft.Identity.Web for the same purpose.

Azure has a feature which is intended to allow Authentication and Authorization to be applied outside of your application code. It's called "Easy Auth". Unfortunately, in the context of App Services it doesn't work with .NET Core and .NET. Perhaps it would be better to say: of the various .NETs, it supports .NET Framework. To quote the docs:

At this time, ASP.NET Core does not currently support populating the current user with the Authentication/Authorization feature. However, some 3rd party, open source middleware components do exist to help fill this gap.

Thanks to Maxime Rouiller there's a way forward here. However, as I was taking this for a spin today, I discovered another issue.

Where are our roles?#

Consider the following .NET controller:

[Authorize(Roles = "Administrator,Reader")]
[HttpGet("api/admin-reader")]
public string GetWithAdminOrReader() =>
"this is a secure endpoint that users with the Administrator or Reader role can access";
[Authorize(Roles = "Administrator")]
[HttpGet("api/admin")]
public string GetWithAdmin() =>
"this is a secure endpoint that users with the Administrator role can access";
[Authorize(Roles = "Reader")]
[HttpGet("api/reader")]
public string GetWithReader() =>
"this is a secure endpoint that users with the Reader role can access";

The three endpoints above restrict access based upon roles. However, even with Maxime's marvellous shim in the mix, authorization doesn't work when deployed to an Azure App Service. Why? Well, it comes down to how roles are mapped to claims.

Let's back up a bit. First of all we've added a dependency to our project:

dotnet add package MaximeRouiller.Azure.AppService.EasyAuth

Next we've updated our Startup.cs``ConfigureServices such that it looks like this:

if (Env.IsDevelopment()) {
services.AddMicrosoftIdentityWebAppAuthentication(Configuration);
else
services.AddAuthentication("EasyAuth").AddEasyAuthAuthentication((o) => { });

With the above in place, either the Microsoft Identity platform will directly be used for authentication, or Maxime's package will be used as the default authentication scheme. The driver for this is Env which is an IHostEnvironment that was injected to the Startup.cs. Running locally, both authentication and authorization will work. However, deployed to an Azure App Service, only authentication will work.

It turns out that directly using the Microsoft Identity platform, we see roles claims coming through like so:

[
// ...
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "Administrator"
},
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "Reader"
},
// ...
]

But in Azure we see roles claims showing up with a different type:

[
// ...
{
"type": "roles",
"value": "Administrator"
},
{
"type": "roles",
"value": "Reader"
},
// ...
]

This is the crux of the problem; .NET and .NET Core are looking in a different place for roles.

Role up, role up!#

There wasn't an obvious way to make this work with Maxime's package. So we ended up lifting the source code of Maxime's package and tweaking it. Take a look:

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
/// <summary>
/// Based on https://github.com/MaximRouiller/MaximeRouiller.Azure.AppService.EasyAuth
/// Essentially EasyAuth only supports .NET Framework: https://docs.microsoft.com/en-us/azure/app-service/app-service-authentication-how-to#access-user-claims
/// This allows us to get support for Authentication and Authorization (using roles) with .NET
/// </summary>
namespace EasyAuth {
public static class EasyAuthAuthenticationBuilderExtensions {
public static AuthenticationBuilder AddEasyAuthAuthentication(
this IServiceCollection services) =>
services.AddAuthentication("EasyAuth").AddEasyAuthAuthenticationScheme(o => { });
public static AuthenticationBuilder AddEasyAuthAuthenticationScheme(
this AuthenticationBuilder builder,
Action<EasyAuthAuthenticationOptions> configure) =>
builder.AddScheme<EasyAuthAuthenticationOptions, EasyAuthAuthenticationHandler>(
"EasyAuth",
"EasyAuth",
configure);
}
public class EasyAuthAuthenticationOptions : AuthenticationSchemeOptions {
public EasyAuthAuthenticationOptions() {
Events = new object();
}
}
public class EasyAuthAuthenticationHandler : AuthenticationHandler<EasyAuthAuthenticationOptions> {
public EasyAuthAuthenticationHandler(
IOptionsMonitor<EasyAuthAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock) {
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
try {
var easyAuthEnabled = string.Equals(Environment.GetEnvironmentVariable("WEBSITE_AUTH_ENABLED", EnvironmentVariableTarget.Process), "True", StringComparison.InvariantCultureIgnoreCase);
if (!easyAuthEnabled) return Task.FromResult(AuthenticateResult.NoResult());
var easyAuthProvider = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-IDP"].FirstOrDefault();
var msClientPrincipalEncoded = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(easyAuthProvider) ||
string.IsNullOrWhiteSpace(msClientPrincipalEncoded))
return Task.FromResult(AuthenticateResult.NoResult());
var decodedBytes = Convert.FromBase64String(msClientPrincipalEncoded);
var msClientPrincipalDecoded = System.Text.Encoding.Default.GetString(decodedBytes);
var clientPrincipal = JsonSerializer.Deserialize<MsClientPrincipal>(msClientPrincipalDecoded);
if (clientPrincipal == null) return Task.FromResult(AuthenticateResult.NoResult());
var mappedRolesClaims = clientPrincipal.Claims
.Where(claim => claim.Type == "roles")
.Select(claim => new Claim(ClaimTypes.Role, claim.Value))
.ToList();
var claims = clientPrincipal.Claims.Select(claim => new Claim(claim.Type, claim.Value)).ToList();
claims.AddRange(mappedRolesClaims);
var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(claims, clientPrincipal.AuthenticationType, clientPrincipal.NameType, clientPrincipal.RoleType));
var ticket = new AuthenticationTicket(principal, easyAuthProvider);
var success = AuthenticateResult.Success(ticket);
Context.User = principal;
return Task.FromResult(success);
} catch (Exception ex) {
return Task.FromResult(AuthenticateResult.Fail(ex));
}
}
}
public class MsClientPrincipal {
[JsonPropertyName("auth_typ")]
public string? AuthenticationType { get; set; }
[JsonPropertyName("claims")]
public IEnumerable<UserClaim> Claims { get; set; } = Array.Empty<UserClaim>();
[JsonPropertyName("name_typ")]
public string? NameType { get; set; }
[JsonPropertyName("role_typ")]
public string? RoleType { get; set; }
}
public class UserClaim {
[JsonPropertyName("typ")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("val")]
public string Value { get; set; } = string.Empty;
}
}

There's a number of changes in the above code to Maxime's package. Three changes that are not significant and one that is. First the insignificant changes:

  1. It uses System.Text.Json in place of JSON.NET
  2. It uses C#s nullable reference types
  3. It changes the extension method signature such that instead of entering services.AddAuthentication().AddEasyAuthAuthentication((o) =&gt; { }) we now need only enter services.AddEasyAuthAuthentication()

Now the significant change:

Where the middleware encounters claims in the X-MS-CLIENT-PRINCIPAL header with the Type of "roles" it creates brand new claims for each, with the same Value but with the official Type supplied by ClaimsTypes.Role of "http://schemas.microsoft.com/ws/2008/06/identity/claims/role". The upshot of this, is that when the processed claims are inspected in Azure they now look more like this:

[
// ...
{
"type": "roles",
"value": "Administrator"
},
{
"type": "roles",
"value": "Reader"
},
// ...
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "Administrator"
},
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "Reader"
}
]

As you can see, we now have both the originally supplied roles as well as roles of the type that .NET and .NET Core expect. Consequently, roles based behaviour starts to work. Thanks to Maxime for his fine work on the initial solution. It would be tremendous if neither the code in this blog post nor Maxime's shim were required. Still, until that glorious day!

Update: Potential ways forward#

When I was tweeting this post, Maxime was good enough to respond and suggest that this may be resolved within Azure itself in future:

Oh, so that's why they removed the name? ๐Ÿ˜ฒ๐Ÿ˜œ Jokes aside, we hope that this package won't be necessary for the future. I know that @mattchenderson is part of a working group to update Easy Auth. Might want to make sure you follow him as well. ๐Ÿ˜

โ€” Maxime Rouiller (@MaximRouiller) January 14, 2021

There's a prospective PR that would add an event to Maxime's API. If something along these lines was merged, then my workaround would no longer be necessary. Follow the PR here.

WCF - moving from Config to Code, a simple WCF service harness (plus implementing your own Authorization)

Last time I wrote about WCF I was getting up and running with WCF Transport Windows authentication using NetTcpBinding in an Intranet environment. I ended up with a WCF service hosted in a Windows Service which did pretty much what the previous post name implies.

Since writing that I've taken things on a bit further and I thought it worth recording my approach whilst it's still fresh in my mind. There's 3 things I want to go over:

  1. I've moved away from the standard config driven WCF approach to a more "code-first" style
  2. I've established a basic Windows Service hosted WCF service / client harness which is useful if you're trying to get up and running with a WCF service quickly
  3. I've locked down the WCF authorization to a single Windows account through the use of my own ServiceAuthorizationManager

Moving from Config to Code#

So, originally I was doing what all the cool kids are doing and driving the configuration of my WCF service and all its clients through config files. And why not? I'm in good company.

Here's why not: it gets *very* verbose *very* quickly....

Okay - that's not the end of the world. My problem was that I had ~10 Windows Services and 3 Web applications that needed to call into my WCF Service. I didn't want to have to separately tweak 15 or so configs each time I wanted to make one standard change to WCF configuration settings. I wanted everything in one place.

Now there's newer (and probably hipper) ways of achieving this. Here's one possibility I happened upon on StackOverflow that looks perfectly fine.

Well I didn't use a hip new approach - no I went Old School with my old friend the appSettings file attribute. Remember that? It's just a simple way to have all your common appSettings configuration settings in a single file which can be linked to from as many other apps as you like. It's wonderful and I've been using it for a long time now. Unfortunately it's pretty basic in that it's only the appSettings section that can be shared out; no &lt;system.serviceModel&gt; or similar.

But that wasn't really a problem from my perspective. I realised that there were actually very few things that needed to be configurable for my WCF service. Really I wanted a basic WCF harness that could be initialised in code which implicitly set all the basic configuration with settings that worked (ie it was set up with defaults like maximum message size which were sufficiently sized). On top of that I would allow myself to configure just those things that I needed to through the use of my own custom WCF config settings in the shared appSettings.config file.

Once done I massively reduced the size of my configs from frankly gazillions of entries to just these appSettings.config entries which were shared across each of my WCF service clients and by my Windows Service harness:

<appSettings>
<add key="WcfBaseAddressForClient" value="net.tcp://localhost:9700/"/>
<add key="WcfWindowsSecurityApplied" value="true" />
<add key="WcfCredentialsUserName" value="myUserName" />
<add key="WcfCredentialsPassword" value="myPassword" />
<add key="WcfCredentialsDomain" value="myDomain" />
</appSettings>

And these config settings used only by my Windows Service harness:

<appSettings file="../Shared/AppSettings.config">
<add key="WcfBaseAddressForService" value="net.tcp://localhost:9700/"/>
</appSettings>

Show me your harness#

I ended up with a quite a nice basic "vanilla" framework that allowed me to quickly set up Windows Service hosted WCF services. The framework also provided me with a simple way to consume these WCF services with a minimum of code an configuration. No muss. No fuss. :-) So pleased with it was I that I thought I'd go through it here much in the manner of a chef baking a cake...

To start with I created myself a Windows Service in Visual Studio which I grandly called "WcfWindowsService". The main service class looked like this:

public class WcfWindowsService: ServiceBase
{
public static string WindowsServiceName = "WCF Windows Service";
public static string WindowsServiceDescription = "Windows service that hosts a WCF service.";
private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public List<ServiceHost> _serviceHosts = null;
public WcfWindowsService()
{
ServiceName = WindowsServiceName;
}
public static void Main()
{
ServiceBase.Run(new WcfWindowsService());
}
/// <summary>
/// The Windows Service is starting
/// </summary>
/// <param name="args"></param>
protected override void OnStart(string[] args)
{
try
{
CloseAndClearServiceHosts();
//Make log4net startup
XmlConfigurator.Configure();
_logger.Warn("WCF Windows Service starting...");
_logger.Info("Global.WcfWindowsSecurityApplied = " + Global.WcfWindowsSecurityApplied.ToString().ToLower());
if (Global.WcfWindowsSecurityApplied)
{
_logger.Info("Global.WcfOnlyAuthorizedForWcfCredentials = " + Global.WcfOnlyAuthorizedForWcfCredentials.ToString().ToLower());
if (Global.WcfOnlyAuthorizedForWcfCredentials)
{
_logger.Info("Global.WcfCredentialsDomain = " + Global.WcfCredentialsDomain);
_logger.Info("Global.WcfCredentialsUserName = " + Global.WcfCredentialsUserName);
}
}
//Create binding
var wcfBinding = WcfHelper.CreateBinding(Global.WcfWindowsSecurityApplied);
// Create a servicehost and endpoints for each service and open each
_serviceHosts = new List<ServiceHost>();
_serviceHosts.Add(WcfServiceFactory<IHello>.CreateAndOpenServiceHost(typeof(HelloService), wcfBinding));
_serviceHosts.Add(WcfServiceFactory<IGoodbye>.CreateAndOpenServiceHost(typeof(GoodbyeService), wcfBinding));
_logger.Warn("WCF Windows Service started.");
}
catch (Exception exc)
{
_logger.Error("Problem starting up", exc);
throw exc;
}
}
/// <summary>
/// The Windows Service is stopping
/// </summary>
protected override void OnStop()
{
CloseAndClearServiceHosts();
_logger.Warn("WCF Windows Service stopped");
}
/// <summary>
/// Close and clear service hosts in list and clear it down
/// </summary>
private void CloseAndClearServiceHosts()
{
if (_serviceHosts != null)
{
foreach (var serviceHost in _serviceHosts)
{
CloseAndClearServiceHost(serviceHost);
}
_serviceHosts.Clear();
}
}
/// <summary>
/// Close and clear the passed service host
/// </summary>
/// <param name="serviceHost"></param>
private void CloseAndClearServiceHost(ServiceHost serviceHost)
{
if (serviceHost != null)
{
_logger.Info(string.Join(", ", serviceHost.BaseAddresses) + " is closing...");
serviceHost.Close();
_logger.Info(string.Join(", ", serviceHost.BaseAddresses) + " is closed");
}
}
}

As you've no doubt noticed this makes use of Log4Net for logging purposes (I'll assume you're aware of it). My Windows Service implements such fantastic WCF services as HelloService and GoodbyeService. Each revolutionary in their own little way. To give you a taste of the joie de vivre that these services exemplify take a look at this:

// Implement the IHello service contract in a service class.
public class HelloService : WcfServiceAuthorizationManager, IHello
{
// Implement the IHello methods.
public string GreetMe(string thePersonToGreet)
{
return "well hello there " + thePersonToGreet;
}
}

Exciting! WcfWindowsService also references another class called "Global" which is a helper class - to be honest not much more than a wrapper for my config settings. It looks like this:

static public class Global
{
#region Properties
// eg "net.tcp://localhost:9700/"
public static string WcfBaseAddressForService { get { return ConfigurationManager.AppSettings["WcfBaseAddressForService"]; } }
// eg true
public static bool WcfWindowsSecurityApplied { get { return bool.Parse(ConfigurationManager.AppSettings["WcfWindowsSecurityApplied"]); } }
// eg true
public static bool WcfOnlyAuthorizedForWcfCredentials { get { return bool.Parse(ConfigurationManager.AppSettings["WcfOnlyAuthorizedForWcfCredentials"]); } }
// eg "myDomain"
public static string WcfCredentialsDomain { get { return ConfigurationManager.AppSettings["WcfCredentialsDomain"]; } }
// eg "myUserName"
public static string WcfCredentialsUserName { get { return ConfigurationManager.AppSettings["WcfCredentialsUserName"]; } }
// eg "myPassword" - this should *never* be stored unencrypted and is only ever used by clients that are not already running with the approved Windows credentials
public static string WcfCredentialsPassword { get { return ConfigurationManager.AppSettings["WcfCredentialsPassword"]; } }
#endregion
}

WcfWindowsService creates and hosts a HelloService and a GoodbyeService when it starts up. It does this using my handy WcfServiceFactory:

public class WcfServiceFactory<TInterface>
{
private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public static ServiceHost CreateAndOpenServiceHost(Type serviceType, NetTcpBinding wcfBinding)
{
var serviceHost = new ServiceHost(serviceType, new Uri(Global.WcfBaseAddressForService + ServiceHelper<TInterface>.GetServiceName()));
serviceHost.AddServiceEndpoint(typeof(TInterface), wcfBinding, "");
serviceHost.Authorization.ServiceAuthorizationManager = new WcfServiceAuthorizationManager(); // This allows us to control authorisation within WcfServiceAuthorizationManager
serviceHost.Open();
_logger.Info(string.Join(", ", serviceHost.BaseAddresses) + " is now listening.");
return serviceHost;
}
}

To do this it also uses my equally handy WcfHelper class:

static public class WcfHelper
{
/// <summary>
/// Create a NetTcpBinding
/// </summary>
/// <param name="useWindowsSecurity"></param>
/// <returns></returns>
public static NetTcpBinding CreateBinding(bool useWindowsSecurity)
{
var wcfBinding = new NetTcpBinding();
if (useWindowsSecurity)
{
wcfBinding.Security.Mode = SecurityMode.Transport;
wcfBinding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
}
else
wcfBinding.Security.Mode = SecurityMode.None;
wcfBinding.MaxBufferSize = int.MaxValue;
wcfBinding.MaxReceivedMessageSize = int.MaxValue;
wcfBinding.ReaderQuotas.MaxArrayLength = int.MaxValue;
wcfBinding.ReaderQuotas.MaxDepth = int.MaxValue;
wcfBinding.ReaderQuotas.MaxStringContentLength = int.MaxValue;
wcfBinding.ReaderQuotas.MaxBytesPerRead = int.MaxValue;
return wcfBinding;
}
}
/// <summary>
/// Create a WCF Client for use anywhere (be it Windows Service or ASP.Net web application)
/// nb Credential fields are optional and only likely to be needed by web applications
/// </summary>
/// <typeparam name="TInterface"></typeparam>
public class WcfClientFactory<TInterface>
{
public static TInterface CreateChannel(bool useWindowsSecurity, string wcfBaseAddress, string wcfCredentialsUserName = null, string wcfCredentialsPassword = null, string wcfCredentialsDomain = null)
{
//Create NetTcpBinding using universally
var wcfBinding = WcfHelper.CreateBinding(useWindowsSecurity);
//Get Service name from examining the ServiceNameAttribute decorating the interface
var serviceName = ServiceHelper<TInterface>.GetServiceName();
//Create the factory for creating your channel
var factory = new ChannelFactory<TInterface>(
wcfBinding,
new EndpointAddress(wcfBaseAddress + serviceName)
);
//if credentials have been supplied then use them
if (!string.IsNullOrEmpty(wcfCredentialsUserName))
{
factory.Credentials.Windows.ClientCredential = new System.Net.NetworkCredential(wcfCredentialsUserName, wcfCredentialsPassword, wcfCredentialsDomain);
}
//Create the channel
var channel = factory.CreateChannel();
return channel;
}
}

Now the above WcfHelper class and it's comrade-in-arms the WcfClientFactory don't live in the WcfWindowsService project with the other classes. No. They live in a separate project called the WcfWindowsServiceContracts project with their old mucker the ServiceHelper:

public class ServiceHelper<T>
{
public static string GetServiceName()
{
var customAttributes = typeof(T).GetCustomAttributes(false);
if (customAttributes.Length > 0)
{
foreach (var customAttribute in customAttributes)
{
if (customAttribute is ServiceNameAttribute)
{
return ((ServiceNameAttribute)customAttribute).ServiceName;
}
}
}
throw new ArgumentException("Interface is missing ServiceNameAttribute");
}
}
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class ServiceNameAttribute : System.Attribute
{
public ServiceNameAttribute(string serviceName)
{
this.ServiceName = serviceName;
}
public string ServiceName { get; set; }
}

Now can you guess what the WcfWindowsServiceContracts project might contain? Yes; contracts for your services (oh the excitement)! What might one of these contracts look like I hear you ask... Well, like this:

[ServiceContract()]
[ServiceName("HelloService")]
public interface IHello
{
[OperationContract]
string GreetMe(string thePersonToGreet);
}

The WcfWindowsServiceContracts project is included in *any* WCF client solution that wants to call your WCF services. It is also included in the WCF service solution. It facilitates the calling of services. What you're no doubt wondering is how this might be achieved. Well here's how, it uses our old friend the WcfClientFactory:

var helloClient = WcfClientFactory<IHello>
.CreateChannel(
useWindowsSecurity: Global.WcfWindowsSecurityApplied, // eg true
wcfBaseAddress: Global.WcfBaseAddressForClient, // eg "net.tcp://localhost:9700/"
wcfCredentialsUserName: Global.WcfCredentialsUserName, // eg "myUserName" - Optional parameter - only passed by web applications that need to impersonate the valid user
wcfCredentialsPassword: Global.WcfCredentialsPassword, // eg "myPassword" - Optional parameter - only passed by web applications that need to impersonate the valid user
wcfCredentialsDomain: Global.WcfCredentialsDomain // eg "myDomain" - Optional parameter - only passed by web applications that need to impersonate the valid user
);
var greeting = helloClient.GreetMe("John"); //"well hello there John"

See? Simple as simple. The eagle eyed amongst you will have noticed that client example above is using "Global" which is essentially a copy of the Global class mentioned above that is part of the WcfWindowsService project.

Locking down Authorization to a single Windows account#

I can tell you think i've forgotten something. "Tell me about this locking down to the single Windows account / what is this mysterious WcfServiceAuthorizationManager class that all your WCF services inherit from? Don't you fob me off now.... etc"

Well ensuring that only a single Windows account is authorised (yes dammit the original English spelling) to access our WCF services is achieved by implementing our own ServiceAuthorizationManager class. This implementation is used for authorisation by your ServiceHost and the logic sits in the overridden CheckAccessCore method. All of our WCF service classes will inherit from our ServiceAuthorizationManager class and so trigger the CheckAccessCore authorisation each time they are called.

As you can see from the code below, depending on our configuration, we lock down access to all our WCF services to a specific Windows account. This is far from the only approach that you might want to take to authorisation; it's simply the one that we've been using. However the power of being able to implement your own authorisation in the CheckAccessCore method allows you the flexibility to do pretty much anything you want:

public class WcfServiceAuthorizationManager : ServiceAuthorizationManager
{
protected static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
protected override bool CheckAccessCore(OperationContext operationContext)
{
if (Global.WcfWindowsSecurityApplied)
{
if ((operationContext.ServiceSecurityContext.IsAnonymous) ||
(operationContext.ServiceSecurityContext.PrimaryIdentity == null))
{
_logger.Error("WcfWindowsSecurityApplied = true but no credentials have been supplied");
return false;
}
if (Global.WcfOnlyAuthorizedForWcfCredentials)
{
if (operationContext.ServiceSecurityContext.PrimaryIdentity.Name.ToLower() == Global.WcfCredentialsDomain.ToLower() + "\\" + Global.WcfCredentialsUserName.ToLower())
{
_logger.Debug("WcfOnlyAuthorizedForWcfCredentials = true and the valid user (" + operationContext.ServiceSecurityContext.PrimaryIdentity.Name + ") has been supplied and access allowed");
return true;
}
else
{
_logger.Error("WcfOnlyAuthorizedForWcfCredentials = true and an invalid user (" + operationContext.ServiceSecurityContext.PrimaryIdentity.Name + ") has been supplied and access denied");
return false;
}
}
else
{
_logger.Debug("WcfOnlyAuthorizedForWcfCredentials = false, credentials were supplied (" + operationContext.ServiceSecurityContext.PrimaryIdentity.Name + ") so access allowed");
return true;
}
}
else
{
_logger.Info("WcfWindowsSecurityApplied = false so we are allowing unfettered access");
return true;
}
}
}

Phewwww... I know this has ended up as a bit of a brain dump but hopefully people will find it useful. At some point I'll try to put up the above solution on GitHub so people can grab it easily for themselves.