Menu

Configuring Orchard Core for AD (AD FS) Authentication

How to set the Orchard Core CMS to use a local domain / Active Directory for authentication? The current Ochard version (1.1) doesn't seem to support it, at least not that I know... It does support Azure AD but that is a different beast. One way to make it work is to install Active Directory Federation Services (ADFS) on your server, which would broaden the AD's compatibility by making it support several additional authentication protocols, amongst which is OpenID which is supported by Orchard.

This post will be focused on how to make Orchard and ADFS work together, not how to install ADFS - but I intend to write another post (effectively, a prequel!) that will provide guidance on how to do it. There's a myriad of configuration options and many choices to be made in the process - most of which, in fact, you don't have to make because they don't apply to your particular problem, but there's noone to tell you that. I'll try to fill the gap by providing information on which steps led me to the solution and which can be ignored... But more on that next time, for now I'll say this: setting up ADFS is not that hard, setting up a Certificate Authority to work with it seeems nightmarishly complicated but isn't, and I think both are worth the effort if you have applications that can authenticate through ADFS... If you centralize the users, they will have to remember - and have the chance to forget - only a single password.

Ok, so how to do this for Orchard? I'm writing this as I have done it on Windows 2019, other versions should be similar and post-2012 versions almost identical, from what I've seen. Note that I'm by no means an expert on authentication protocols, I'm just documenting the setup that worked for me in order to help others trying to do the same thing.

 

AD FS Application Group

Configuring ADFS for this is dead simple when you know where to go: type "AD FS" in the start menu to filter down to "AD FS Manager" and then start it (alternatively, it is accessible from the Tools menu of the Server Manager app). Right-click Application Groups in the left list and click Add Application Group. Select the Web Browser Accessing a Web Application option and give it a name. Click Next to go to native application setup: here you get a generated Client ID (you'll need that for Orchard configuration) and the option to add one or more redirect URLs. From what I've seen, Orchard always uses its root as a redirect URL so add that. This URL must coincide with Orchard configuration, otherwise the authentication will throw an error. Multiple URLs can be added which is handy because you can configure a single application group to work with a development Orchard instance you run in Visual Studio (something like https://localhost:5001 - yes, it will work with localhost as well) and a production server (https://orchard.mydomain.local). Click Next - I left this setting at "Permit everyone" - and then two Next's and one Close to finish. And now you have a configuration that can work with Orchard. It can be improved, but more on that later.

One detail that might be of interest is that the relying party identifier in the web application properties is the same as the native app's client ID. This is obviously right and it works that way, but the form explicitly gives "https://fs.contoso.com/adfs/services/trust" as an example for a relying party identifier - so, a URL and not an ID: a false lead.

 

Orchard OpenID Authentication Client

Orchard Authentication Client Settings for ADFSNow log in to Orchard's dashboard, go to Configuration - Features, filter the list for "OpenID" to find the OpenID Client and click the Enable button next to it. While you're at it, find the Users Registration feature and enable it if it isn't already.

Now move on to Security - OpenID Connect - Settings - Authentication Client. Set the display name to something that your users can recognise, because it will be displayed on an alternative login button - for example, "[mydomain] AD FS" (it would probably be less scary for them without "AD FS" but I kept that part so that I myself know what it is). The Authority field is the base URL where your Federation Service server accepts OpenID requests. It's something like https://fs.yourdomain.local/adfs, and you can check this from your AD FS Manager app: expand the Service node on the left and then select Endpoints. It will list a bunch of available URLs for different protocols and uses, none of them will be exactly /adfs/ but all will start with it, and several will be explicitly tagged as OpenID. I suppose Orchard knows how to add the rest of the URL by itself.

Next in the Orchard settings is the ClientID - you should copy it from the application group in AD FS. (If you forgot to do it, open AD FS Manager, select Application Groups on the left, then double click your application in the list and double click the native app in the dialog that opens... You will see the client ID, copy it and paste it back into Orchard). For basic configuration, all that is left is to select the authentication flow: I myself got it to work by checking "Use id_token response type" in the Implicit Authentication Flow section. The various callback and redirect paths I left at default (blank) as well as the scope (blank) and response mode (form_post).

A word of caution is in order here: if you're experimenting with settings, be careful around the Client Secret field (it isn't visible for implicit authentication flow). I'm not sure if it's a bug in Orchard but once set, the field cannot be reset directly as it doesn't show the saved value: there is nothing to delete. (Looking at the code, I'm under the impression that selecting a different flow type could reset it, though, but I was unable to find the right combination, possibly because the Edge browser joined in on the game, see below). Why is this important? The newer versions of AD FS report an error if you send a client secret where it is not needed, so nothing works because of it. It's quite a puzzle, you'll see the client secret field empty but AD FS will complain that it receives one. To make things worse, I allowed the Edge browser to remember my login password and for some reason it seemed to fill the Client Secret field with it, making it non-empty even if I didn't set it and making it look like Orchard did in fact show the saved value... So just be careful with it, don't say I didn't warn you.

 

User Registration

Now go to Security - Settings - User Registration, make sure that in the top combo box you have AllowRegistration or AllowOnlyExternalUsers selected. The first will allow anyone to register in Orchard (locally or through external OpenID authentication) and the second will allow only externally authenticated users to register. There seems to also be an option that the user log in with a local name and then link an external login to it - something like this can be done by logging in as a local user, then opening the user menu (usually top right and showing the username), clicking on External Logins and then logging in with an additional external user. But I haven't experimented much with this, my goal for Orchard was - as stated above - to use external users only.

At this point, you should be able to log in with the external user: log out, go to the login page and you should see a "Use another service to log in" label and a button with the AD FS name as configured previously. But when you do, Orchard will ask you to type in your local username (also provide you with an ugly generated default) and an e-mail.

 

Retrieving additional claims

To make this process smoother, we can configure it to retrieve the username and e-mail from AD. First we need to tell the AD FS to send additional stuff: go to AD FS Manager, select Application Groups, double click the group created previously, and double click its Web application. Go to the Instance Transform Rules tab. Click Add Rule: it will select "Send LDAP Attributes as Claims" by default for rule template, so click Next and add rules for different attributes. I added these pairs (LDAP attribute => Outgoing claim type):

  • SAM-Account-Name => "SAMAccountName" (I typed the claim name in myself, it isn't present in the dropdown... It doesn't matter what name you give it as long as it doesn't clash with another one and you use the same name in the Orchard registration script afterwards)
  • E-Mail-Addresses => E-Mail Address

For experimentation, I added various other attributes (given name, surname) to get the user's name - but haven't found a place to store them. It doesn't seem that Orchard cares for having anything more than a username and e-mail for a user.

Click OK to save this rule. Some sites mention that additional scopes have to be added here to be able to retrieve all the claims (specifficaly the allatclaims scope): they are in the Client Permissions tab and can be checked but everything seems to work for me with only the default - openid - scope checked. By default, Orchard uses the "openid" and "profile" scopes. (I suppose you could restrict your rules to a different scope and add that scope on the Orchard's Authentication client settings page).

We now need to modify Orchard's user registration script to use this data when creating a user. It's in Security - Settings - User registration, you need to check "Use a script to generate username based on etc." to make it editable. My script looks like this:

if(context.loginProvider == "OpenIdConnect")
{
  var unameClaim = context.externalClaims.find(c => c.type == "SAMAccountName");
 
  if(unameClaim != null)
  {
      context.userName = unameClaim.value;
  }
}

You can also uncomment the already provided line -

log("warning", JSON.stringify(context));

- to see the full structure of the returned claims in the log. This can help troubleshoot problems - for example, I experimented with code authentication flow but couldn't get ADFS to send any of the additional claims whatever I did, although authentication worked.

(By the way, the language used in the script is JavaScript, and a rather recent version it seems if it can handle these LINQ-like expressions... Haven't been able to find this information anywhere, but luckily I made a couple of syntax errors and got yelled at by some kind of JavaScript engine, which made it clear).

Having set this, you can now also tick the checkboxes below the script - "Do not ask username", "Do not ask email address" and "Do not create local password for external users" because everything can now be retrieved from AD.

We can even go a step further and tie a domain group to a local Orchard role. This is done using the same Issuance Transform Rules tab as before in the Web application properties: click Add Rule as before but this time select "Send Group Membership as a Claim". Here you can select a domain group and have the ADFS send a claim if the user is a member - there's a predefined "Group" claim type (I'm not sure if that's what it is meant for, looks like it is) and then type in a claim value - which I set to the group name.

But for my configuration I opted for a simpler solution: if the user logs in as the domain administrator, I'll assign it Orchard's administrator role. I think it's cleaner that way: ordinary users are just business users (they deal with content) and an administrator logs in rarely just to administer stuff. I used the following script (it can be set through Security -> Settings -> User Login, check "Use a script to set roles based on etc" to make the field editable):

if (context.loginProvider == "OpenIdConnect")
{
  // if it's the domain admin, accept it as the local administrator
  var unameClaim = context.externalClaims.find(claim => claim.type == "SAMAccountName");
  if(unameClaim != null && unameClaim.value == "Administrator")
  {
    context.rolesToAdd.push("Administrator");
  }
}

And, by and large, that's all it takes... Admittedly, it requires some time to get done but it's not very hard to do - and wouldn't be too hard to configure differently, I suppose. The bigger part of work lies, of course, in installing the ADFS and the CA (if needed) for it, but that is a different story which I hope to get to tell soon.

Repairing the WS-Management service on the Windows server

Some features of the Windows 2019 server can easily and inadvertently be incapacitated and many seem to get screwed up on their own. And it is quite possible that one error triggers another and getting out of that tangled mess is very hard and time consuming. It takes a lot of time just to restore the basic functionality of the Windows server.

It took me months of on-and-off investigation to figure out how to persuade the DNS not to add an entry for each and every one of the server's IP addresses, because when it does so, the DNS queries return these different IPs in a round-robin fashion and this causes errors. And there are no good solutions on the internet, they all say that "Register this connection's addresses in DNS" should be unchecked, but that's not enough: you also need to unbind the DNS server from all IP addresses but one (it's in Properties-Interfeaces in the DNS app) and also removing the IPs from the Name Servers tab in your DNS zone's properties.

After I did this - and it may or may not be related - the Server Manager started reporting errors like "The WS-Management service is configured to not accept any remote shell requests." and because of this I was now unable to install or uninstall anything. I solved the by using PowerShell equivalent commands, but it worked for some tasks and for some it didn't. The predominant solution suggested on the net was to turn on AllowRemoteShellAccess using gpedit.msc, but it required me to do gpupdate /force and this in turn didn't work because of some policy replicating problem on Windows. And all the time, the stuff that worked in Windows still worked but it was a gamble trying to change anything on the server. There was a bunch of solutions on the internet for this but none of them worked: just using the File Explorer to browse through the problemmatic SYSVOL share, I got the impression that the domain shortcut that was there pointed to the wrong server - it actually pointed to the domain but that resolved to the wrong server's IP... I believe I did some tinkering in the DNS that may have helped but wasn't the final answer. In the end, a brute force solution was required, I just copied SYSVOL contents from the other domain server. (Having two domain servers could have been the root of the problem anyway, and possibly the DNS changes).

Back to the original WinRM problem, I was now able to turn AllowRemoteShellAccess on, to no effect. So I tried other suggestions - and once again, most solutions revolved around the same repeated stuff, none of which worked. Running winrm quickconfig returned "WsManFault" with error number -2144108526 0x80338012, saying "The client cannot connect to the destination specified in the request" and that I should run winrm quickconfig to configure the WinRM service. Issuing dir wsman:\localhost\Shell to check the active configuration said that the path does not exist, even if doing dir wsman:\ listed localhost as a result. A StackOverflow post suggested this bit of PowerShell scripting that finally shed some light on this:

$c = Get-Credential
New-CimSession -ComputerName localhost -Credential $c

Running this for localhost reported an error but did work if I used the name of my server. So WinRM worked, but not on localhost?!

I found the final bit of information on Robin CM's IT Blog, here somebody finally approached the problem instead of repeating the same mantras already present on the internet. Running netstat -aon | find "5985" to find what IP address is the WinRM port (5985) bound to (and note that this is command prompt, not Powershell) returned:

TCP    192.168.24.82:5985      0.0.0.0:0              LISTENING       4

So the service really is bound to the external IP and not localhost!

The blog post suggested I do netsh http delete iplisten 192.168.24.82, but it seemed too drastic to me - from what I understand, this is actually the IIS binding (I could be wrong) and I didn't want IIS listening on all IP addresses. So I simply added localhost to the list (this was OK for IIS) and it worked:

netsh http add ipaddress=127.0.0.1

(note that I also did iisrestart, which may or may not be necessary). The Server Manager now works and winrm quickconfig stopped erroring out... In the end, I don't have AllowRemoteShellAccess configured in the GPO at all, it allows the connections by default, and dir wsman:\localhost\Shell now shows this with no complaints.

Now let's see what else is faulty... How about "Windows cannot access the specified device, path, or file." when the settings app tries to run control.exe? No - maybe next year.

Short tips to getting Excel working with ASP.Net Core OData service

I've been tinkering with an Asp.Net Core OData service designed to be consumed by the Excel client... There are a lot of details that will screw your brains if you don't get them right, and it doesn't help that ASP.Net is a fidgety beast in that bits move around in each version and there are more examples on the net that don't build than the ones that do. I could do a separate post for each of them since it's very hard to get relevant information, but for the time being I'll just list them here.

For one, older versions of Excel (pre-2016) have some OData support but it doesn't seem to be adequate, it's best to install the Power Query plug-in and use that for OData. In 2016 and later, be careful as there are two OData options - under Get External Data and under New Query... You want the second one.

If you need authentication, don't count on using OAuth (which was what I would have expected - OData, OAuth, right?). Excel supports basic, windows, web api (i.e. some kind of a security key) or organizational account (which I suppose is AD). My users are stored in my application database, so the only way was to use the basic authentication... Which is so deprecated that Asp.Net guys refuse to support it and I had to roll my own. (Not too difficult, there are examples on the net, but additional work nevertheless).

There's a ton of examples on how to implement a trivial Asp.Net OData service that returns data from a single Entity Framework-mapped table. I haven't found (m)any that show how to use more complex SQL queries. Because, if you need this data in Excel, you'd like to have complex query - or not? Apparently nobody thought about that. There's an open source component called DynamicODataToSql that understands the OData data-shaping commands and can convert them to SQL. Well - only if the SQL you start with contains a single table name... Uhm, at least it could be a view in the database, I guess EF can do that as well. But with a couple of modifications, this component can be persuaded to at least treat what you give it as a nested SQL query (turning it effectively into a table) and add its magic on top of that.

Also, Asp.Net doesn't know how to return appropriate errors through OData, it spits out an HTML error page whereas a Web API is supposed to return XML or JSON. So, more manual labour: solutions exist online on how to add a filter that does this, but it's not supported out of the box. Still, even with this, Excel seems to sometimes ignore some errors. Especially in the query editor window, the preview that it shows isn't necessarily the data that it pulled from the server at that precise moment (as in: you see your code throwing an exception on the server side but Excel pays no attention and still shows data). The best way to check if the service works seems to be to load the data into a sheet and refresh it from there.

And the final headbanger - for now, at least - was how to get Excel to do server-side data shaping. Because, for unknown reasons, it sometimes decides to load everything from the database and then filter data by itself... Which is insane. One important bit I found that makes or breaks this is the URL you give Excel to access the data. If you target your OData controller directly (e.g. http://localhost/odata/MyData), everything will seemingly work but the data will be filtered on the client. If the URL points to the base OData directory (e.g. http://localhost/odata), Excel's editor will add a Navigation step to the query to select the controller - and with this the server-side filtering will work. Now, I'm talking only about filtering as I'm not sure if Excel supports other OData stuff like grouping: filtering is fundamental and I'm OK with counting the rest - especially given all the problems listed above - as a bonus.

Subscribe to this RSS feed