Securing a Web API with Windows Server 2012 R2 ADFS and Katana
Last week I wrote a post about how to use Katana and Windows Azure AD to secure an MVC4 Web API, and showed how to use AAL to build a Windows Store client in just few lines of code.
This week I’d like to show you how you can apply the exact same approach when using the new OAuth2 & JWT support in Windows Server 2012 R2 ADFS; once again, this was one of the most frequent requests after my //BUILD/ session last month hence I hope this will make you guys happy .
Note: I am writing this post mostly for unblocking some people who asked for help right now, and for demonstrating the use of Katana in Web API scenarios a where the directory is kept on-premises. The ADFS guys will publish lots of official documentation in the coming weeks/months, and it goes without saying that you should always refer to the official documentation.
Walkthrough
The scenario we want to implement is pretty simple: we want to restrict access to an MVC4 Web API to the users of a given on-premises AD instance, which happens to be using Windows Server 2012 R2 ADFS (just “ADFS” from now on). Furthermore, we want to expose the Web API to the user via a Windows Store application.
This is the exact same scenario we discussed last week, and the implementation is nearly the same; hence instead of writing the same things I am going to ask you to read last weeks’ post first. Once you have done that, and you have working bits in Visual Studio, you can come back here and I’ll show you how to modify things to make the ADFS scenario work.
Another prerequisite is that you must have a Windows Server 2012 R2 ADFS instance available: you can download the Preview here.
Setting Up the Web API Project
Alrighty, do you have the Windows Azure AD based scenario working? Fantastic. Let’s start to modify things to work with ADFS starting form the Web API project.
The good news is that in a short you will not have to change any code at all in order to switch from Windows Azure AD to ADFS and vice versa: Katana will include middleware that will work with both. The bad news is that today the middleware we used for validating incoming tokens is hardwired to Windows Azure AD. However do not despair, as working around it is pretty straightforward.
Modify your Startup.cs class as following:
public class Startup { public void Configuration(IAppBuilder app) { app.UseWindowsAzureBearerToken(new WindowsAzureJwtBearerAuthenticationOptions() { Audience = "https://contoso100.com/API/KatanaADFS_WebAPISample", Tenant = "sts.contoso100.com", MetadataResolver = new ADFSMetadataResolver() }); } }
What did I do here? I changed the Audience and Tenant values to refer to my ADFS scenario, and I added an assignment for the MetadataResolver property. What is the MetadataResolver and how is it used in our scenario? Well, the MetadataResolver is the class used by the middleware to reach out to the authority issuing tokens for your service, and acquire the coordinates that must be used for validating incoming tokens: signing key, issuer, and so on. In the Windows Azure AD scenario we did not have to specify it, as the WindowsAzureBearerToken middleware uses a class available out of the box which is hardwired to use the metadata path of Windows Azure AD tenants. Hence, if we want to work around the current limitation all we need to do is providing a MetadataResolver which goes to ADFS instead. Once again, soon this workaround will no longer be necessary.
Basically all you need to do is creating a new class in the file ADFSMetadataResolver.cs and paste the following:
using Microsoft.Owin.Security.WindowsAzure; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IdentityModel.Metadata; using System.IdentityModel.Tokens; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.ServiceModel.Security; using System.Xml; namespace KatanaADFS_WebAPISample { public class ADFSMetadataResolver : IMetadataResolver { internal class EndpointMetadata { public DateTime ExpiresOn { get; set; } public string Issuer { get; set; } public IList<SecurityToken> SigningTokens { get; set; } } private const string SecurityTokenServiceAddressFormat = "https://sts.contoso100.com/federationmetadata/2007-06/federationmetadata.xml"; private static readonly XmlReaderSettings _SafeSettings = new XmlReaderSettings { XmlResolver = null, DtdProcessing = DtdProcessing.Prohibit, ValidationType = ValidationType.None }; private ConcurrentDictionary<string, EndpointMetadata> _metadata = new ConcurrentDictionary<string, EndpointMetadata>(); public ADFSMetadataResolver() { CacheLength = new TimeSpan(1, 0, 0, 0); } public TimeSpan CacheLength { get; set; } public string GetIssuer(string tenant) { return GetMetadata(tenant).Issuer; } public IList<SecurityToken> GetSigningTokens(string tenant) { return GetMetadata(tenant).SigningTokens; } private EndpointMetadata GetMetadata(string tenant) { if (!_metadata.ContainsKey(tenant) || _metadata[tenant].ExpiresOn < DateTime.Now) { using (var metaDataReader = XmlReader.Create(string.Format(CultureInfo.InvariantCulture, SecurityTokenServiceAddressFormat, tenant), _SafeSettings)) { var endpointMetadata = new EndpointMetadata(); var serializer = new MetadataSerializer() { CertificateValidationMode = X509CertificateValidationMode.None }; MetadataBase metadata = serializer.ReadMetadata(metaDataReader); var entityDescriptor = (EntityDescriptor)metadata; if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id)) { endpointMetadata.Issuer = entityDescriptor.EntityId.Id; } var tokens = new List<SecurityToken>(); var stsd = entityDescriptor.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First(); if (stsd == null) { throw new InvalidOperationException("No SecurityTokenServiceType descriptor in metadata."); } IEnumerable<X509RawDataKeyIdentifierClause> x509DataClauses = stsd.Keys.Where(key => key.KeyInfo != null && (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).Select(key => key.KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First()); tokens.AddRange(x509DataClauses.Select(token => new X509SecurityToken(new X509Certificate2(token.GetX509RawData())))); endpointMetadata.SigningTokens = tokens.AsReadOnly(); endpointMetadata.ExpiresOn = DateTime.Now.Add(CacheLength); lock (_metadata) { _metadata[tenant] = endpointMetadata; } } } return _metadata[tenant]; }
} }
…an that’s all you need to modify the Web API to accept tokens from ADFS.
Setting Up the Web API in ADFS
Next, we are going to tell ADFS about our Web API.
As mentioned earlier, you’ll need to set up your own instance: I was lucky to have the ADFS guys themselves to set one for me! (Once again, you can get the Windows Server 2012 R2 Preview bits here. ADFS is one of the features on board)
You manage the new ADFS just like the old one, via MMC console. You can start it form the Server Manager as shown below.
The UI offers the familiar experience you learned to use with 2.0; and just like in 2.0, registering a resource in ADFS is done by creating a “relying party trust”. To do that, you can hit “add relying party trust” on the action pane at any time.
You’ll be presented with the usual RP trust wizard, with some small changes here and there.
Hit Start.
We are going to create the resource entry manually, there’s no metadata describing the Web API. Choose “Enter data about the relying party manually”.
The first step gathers the name you want to assign to the rp trust entry. Needless to say, pick something that you’ll recognize later!
The subsequent step gives you the chance of producing an ADFS1.x compatible RP entry; that’s definitely not the case here. Pick “AD FS profile” and move forward.
Skip through the next screen and leave all of the certificate related features as is. You’ll land on the Configure URL screen.
A relying party trust entry can be used to engage with the RP via multiple protocols. The screen offers WS-Federation and SAML P: we don’t need either of those, and the OAuth2 endpoints are automatically enabled. Leave everything as is and click Next.
Now, this part is very important: you need to provide the identifier that will be used in the OAuth2 flows to indicate that the resource being accessed is your Web API, so that ADFS will know what settings should be apply.
Choose a URI that is representative of your service: here I am picking one analogous to the one I used for Windows Azure AD, modified to indicate that this resource is protected by ADFS, but I could have used exactly the same identifier if I would have chosen to. Add your identifier, click add and click next.
The next screen is new with Windows Server 2012 R2: it allows you to configure multi-factor authentication settings for your RP. It is an *awesome* feature, that I know many of you have been eagerly waiting for, but for the sake of simplicity I’ll ignore it here. Leave everything as is and click Next.
The next screen allows you to establish the baseline policy for the resource: are all users allowed by default to get a token for this resource, or is the opposite true? Keep the “Permit” policy and move forward.
That’s it, your RP trust entry is now in!
As usual, a brand new RP trust does not issue any claims of its own: you need to tell ADFS which claims should be issued for your Web API. The wizard offers to open the claim rules dialog: hit close and you’ll be presented with the UI you need to add rules to your RP trust.
ADFS supports various rule types, a topic worth of multiple posts in itself. Here we’ll stick to the bare minimum by adding one issuance rule. Click “Add Rule..”.
You’ll land on another wizard, designed to help you to easily define claim rules. We are going to do something really straightforward, issue an email claim sourced form the user attributes kept in AD. select the “Send LDAP Attributes as Claims” option and click next.
Give a meaningful name to the rule, pick Active Directory as the attribute store, and pick the email entry on both the LDAP attribute and outgoing claim type dropdowns.
Hit finish and then hit OK until all dialogs are gone. Congratulations: your Web API is now configured in ADFS.
Setting Up a Test Client Project in ADFS and in Visual Studio
Now that we took care of the Web API, all that’s left is modifying & registering the client with ADFS.
Now, here there’s the kicker: if you’d want, you could leave the client project *exactly* as we developed it in the Windows Azure AD scenario: AAL offers you the exact same methods signature whether you connect to Windows Azure AD or ADFS. However there are things that you can do on the client that will prepare your app to take advantage of some extra ADFS features, such as workplace join related access control. I won’t be talking about those in this walkthrough, however it is worth it to do all the due diligence at this point so that you won’t have to revisit the client setup once you’ll want to learn more about those features. That said, let’s get to work.
Obtaining the MS-APP:// Value for the Client
As for the Windows Azure AD scenario, we need to register the client with our authority (in this case ADFS) before being able to request tokens from it. A key difference between the two scenarios is that whether Windows Azure AD requires you to explicitly allow a client to call a given Web API, in ADFS every registered client can call every registered Web API (modulo access policies, of course).
Various ADFS features are available to Windows Store applications only if they use the WebAuthenticationBroker in SSO mode (if you are not familiar with the concept: backgrounder here). We’ll see in a moment what that means for AAL, but for now this has a very practical consequence on the registration process: the client must use its ms-app://appSID address as returnURI, hence we need to find out that value before being able to register the client with ADFS.
The most common way of doing so is to temporarily add the following snippet in your Windows Store app (OnNavigatedTo is a good candidate), put a breakpoint and wait to hit that.
Uri redirectURI = Windows.Security.Authentication.Web.WebAuthenticationBroker.GetCurrentApplicationCallbackUri();
Once there, you can copy the value of the ms-app:// URI, stop the debugger and remove the code.
Very good! With the ms-app:// value in place, we can now proceed with the registration. At the cost of being pedantic, I’ll highlight that all of the above is not a strict requirement: I could have used an arbitrary URI (like I did for the Windows Azure AD case) but then I would not have been able to take advantage of the full gamut of new ADFS features.
Registering a Client in ADFS
You might have noticed from the screenshots of the ADFS MMC that there was no entry for clients. The reason is that clients ar enot managed throuhg UI, but via PowerShell. You can use the Server Manager to access the ADFS PowerShell module and open a prompt.
The cmdlet we want to use is Add-AdfsClient. It is the most straightforward cmdlet you’ll ever find in this space ! Usage below:
Basically we just need to pass the client name, the clientID (I will reuse the same GUID I used for the Windows Azure AD scenario) and the redirect URI (this is where you’ll need the ms-app://).
Right after the Add-AdfsClient client I call Get-AdfsClient to ensure that the entry looks like I expect.
PS C:\Users\vittorio> Add-ADFSClient -Name "KatanaADFS_W8ClientSample" -ClientI d "3fb2a37f-4ced-409c-937c-dddd776f4dfd" -RedirectUri "ms-app://s-1-15-2-1101140 336-4090662585-1905587327-262951538-2732256205-1306401843-4235927180/" PS C:\Users\vittorio> Get-AdfsClient -ClientId "3fb2a37f-4ced-409c-937c-dddd776 f4dfd" RedirectUri : {ms-app://s-1-15-2-1101140336-4090662585-1905587327-262951538-273 2256205-1306401843-4235927180/} Name : KatanaADFS_W8ClientSample Description : ClientId : 3fb2a37f-4ced-409c-937c-dddd776f4dfd BuiltIn : False Enabled : True ClientType : Public
Believe it or not, that’s it. your client is now registered.
Update the Client Project
The last thing we need to do it tweaking the client. As I already mentioned, I could have left it (nearly) unchanged, but wanted to take the opportunity to show off some more features of AAL.
The client UI and structure remain the same: all we change is the handler for the click event.
1: private async void myButton_Click(object sender, RoutedEventArgs e)
2: {
3: AuthenticationContext authenticationContext =
4: new AuthenticationContext("https://sts.contoso100.com/adfs", false);
5: AuthenticationResult _authResult =
6: await authenticationContext.AcquireTokenAsync(
7: "https://contoso100.com/API/KatanaADFS_WebAPISample",
8: "3fb2a37f-4ced-409c-937c-dddd776f4dfd");
9:
10: string result = string.Empty;
11: HttpClient httpClient = new HttpClient();
12: httpClient.DefaultRequestHeaders.Authorization =
13: new AuthenticationHeaderValue("Bearer", _authResult.AccessToken);
14: HttpResponseMessage response =
15: await httpClient.GetAsync("http://localhost:22102/Api/Values");
16:
17: if (response.IsSuccessStatusCode)
18: {
19: result = await response.Content.ReadAsStringAsync();
20:
21: }
22: MessageDialog md = new MessageDialog(result);
23: IUICommand x = await md.ShowAsync();
24: };
The first difference is in line 4. We initialize the AuthenticationContext with the address of the ADFS service (it has to end with adfs for AAL to recognize it as such) and we turn off authority validation. On the latter: Windows Azure AD exposes metadata for validating that the URL is of the right format, to prevent redirect attacks, as of today ADFS does not offer that feature.
The second difference is in how we invoke AcquireTokenAsync, lines 5 to 8. Whereas in the Windows Azure AD scenario I used the richest overload of AcquireTokenAsync, here I am using the most essential one: all I am passing is the ID of the target resource, the Web API, and the client ID I used when registering the app in ADFS. Given that I omitted the redirect URI, AAL automatically uses the WebAuthenticationBroker in SSO mode which in turn is, as mentioned, a prerequisite for many new ADFS goodies we’ll discuss in the future.
Testing the Solution
What’s left? Spinning this bad boy, of course! Just like we did for the IWndows Azure AD tutorial, hit F5 for starting the Web API and right click->debug->start new instance to start the client.
Once the UI shows up, hit the button on the left.
Here AAL will bring out the ADFS authentication experience: as you can see from the screenshot below, it scales gracefully to accommodate form factors as the WebAuthenitcationBroker’s and is fully customizable (the fruity logo is courtesy of Sam, who’s using the same instance (technically it’s HIS instance ) for demonstrating during his presentations how to modify ADFS’ UI).
Type your credentials and hit enter.
Success! Just like we saw in Windows Azure AD, we get a dump of the claim values the Web API received in the token from ADFS. If you squint a bit you can see that among those there is the email, as established by the issuance rule we created earlier; all the other ones are JWT structural claims.
Wrap
I might be biased, but I believe that the native client+Web API this scenario is the quintessential manifestation of the on-premises-cloud symmetry in our offering. Thanks to the common set of capabilities and consistent approach in Windows Azure AD and ADFS, and to the fact that one single library (AAL) serves both, you are basically taking the same solution and switching topology with just few adjustments in the parameters here and there. Most of the extra code in this post is to work around the current preview status of some of the moving parts, and the need for it will go away very soon.
I hope you’ll choose to play with this scenario too, the new ADFS has great features, here I just scratched the proverbial surface, and I am sure you’ll appreciate how easily you can take advantage of its new capabilities. If you have feedback on the libraries I am all ears (note that this works with AAL .NET on classic desktop apps too!!) – and if you have feedback on ADFS I can’t act directly on it, but I will make sure to relay it to the guys in the ADFS team. Have fun!
2 Comments