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.

Subscribe to this RSS feed