Call a Web API Without Knowing in Advance Its Resource URI or What Authority It Trusts
Courtesy of the daylight saving switch, which made the weekend just a tad longer, here there’s a quick post to show a relatively small but powerful feature of ADAL, the AuthenticationParameters class.
In a nutshell: I’ll show you how you can write a client to consume a Web API secured by AD – knowing nothing at development time about the Web API’s security requirement.
Resource-Driven Authority Discovery
Most of the ADAL code samples you have seen here follow the same (hopefully easy) pattern:
- Create an AuthenticationContext instance to represent in your code the authority (Windows Azure AD tenant/ADFS instance) trusted by your target resource
- Use the AuthenticationContext instance to obtain a token (via any of the AcquireToken overloads) for the target resource
- Use the token to call the service
That pattern assumes that you already know #1 the authority you want to work with, and 2# the identifier by which the target resource is known by that authority. That is often a sound assumption (after all, as of today you do have to explicitly configure permissions between a client and a resource, at least in AAD) but it does not always hold. You might have created a generic client, meant to be downloaded from some central store and used by multiple tenants that are unknown at development and publication time.
Sometimes, all you know are the coordinates of your client app itself (id, returnURI) and the physical URL (not the URI) of the resource you want to access.
Well, good news everyone! The Bearer Token Usage P.S. from the Oauth2.0 Authorization Framework defines a mechanism through which a protected resource can – upon receiving a request without adequate authentication credentials – challenge the caller by passing back information that the client could use to obtain the necessary credentials. Trivializing things a bit: you could say that if you call a Web API without including the token it expects, the Web API will reply back with the necessary data (usually the authority it trusts and the Web API’s identifier) for you to obtain the right token and retry. This handily takes care of the aforementioned case: who cares if you don’t know the authority and the id of the Web API in advance, when the Web API itself can tell you the first time you call it!
The challenge format is super simple and you could easily parse it manually, however we decided to make things absolutely frictionless for you and baked support for it directly in the library’s object model.
The responsibility of generating the challenge is on the resource developer: that means that if besides the client app you also own writing the Web API, it’s up to you to inject the necessary logic.
The main ADAL sample on code gallery does demonstrate this approach. However we wrote that sample in the pre-OWIN era, when you had to handle all the resource side code (JWT interception, validation, ClaimsPrincipal creation, etc) by hand. I wanted to make sure you know how to take advantage of this feature also when you are creating your Web API with the VS2013 templates (AAD tutorial here, ADFS tutorial here) and OWIN middleware. As of today that entails customizations that might not immediately clear, or at least weren’t for me: luckily I had the help of David Matson, a developer on the ASP.NET team that knows this stuff inside out and also happens to be the fastest typist I’ve ever seen working in the VS IDE Thanks David!
Here there’s how I am going to break this down: first I’ll walk you though the code you need to add to the Web API project to generate the challenge, then I’ll show you what parts of ADAL you can use on the client to make sure your app understands the challenge. That done, we’ll give it a spin and then we’ll make some larger scope considerations.
Emitting a Challenge From Your Web API
Let’s start by creating a Web API project configured to work with organizational accounts, as described here.
That will provide us with the perfect starting point, a Web API project already provisioned in Windows Azure AD and already configured with the necessary OWIN middleware to validate JWTs from the Windows Azure AD tenant we used for creating the project.
The goal is to modify the behavior of the authentication OWIN middleware to produce a suitable Challenge message whenever a 401 response is warranted.
To do that, we will need to play a bit with the lower level components that ASP.NET uses for working with OWIN in Web API projects. Go to the Solution Explorer, right click on the Web API project and choose Manage NuGet Packages. Search for Microsoft.AspNet.WebApi.Owin and once you find it install it.
Excellent. That done, go to the Controllers folder and open the ValuesController.cs. We are going to use the default code generated by the template. Take a look at the controller declaration:
[Authorize] public class ValuesController : ApiController { // GET api/values public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; }
The template decorates the controller with [Authorize] to enforce that the caller will be granted access only upon presentation of the correct credentials – in this case, a JWT from the right AAD tenant.
Now, the Owin middleware already has its own challenge generation logic – but it does not generate the kind of message we want. David tells me that as of today the strategy that yields the best code economy consists in
- creating a filter that will turn off the default challenge behavior
- creating a filter that will pick up that functionality and produce the message we want
In practice, this means that the code above will change to look like the following:
[HostAuthenticationNoChallenge("Bearer")] [WindowsAzureADChallenge] [Authorize] public class ValuesController : ApiController { // GET api/values public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; }
The first attribute does #1, the second one does #2. Let’s define those classes, starting with HostAuthenticationNoChallenge. Here there’s the code:
public class HostAuthenticationNoChallengeAttribute : Attribute, IAuthenticationFilter { private readonly IAuthenticationFilter _inner; public HostAuthenticationNoChallengeAttribute(string authenticationType) { _inner = new HostAuthenticationAttribute(authenticationType); } public Task AuthenticateAsync(HttpAuthenticationContext context,
CancellationToken cancellationToken) { return _inner.AuthenticateAsync(context, cancellationToken); } public Task ChallengeAsync(HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken) { return Task.FromResult(0); } public bool AllowMultiple { get { return _inner.AllowMultiple; } } }
That does not have a lot going identity-wise, hence I am mostly using this as boilerplate logic for enabling the next class to do its job:
public class WindowsAzureADChallengeAttribute : Attribute, IAuthenticationFilter { public Task AuthenticateAsync(HttpAuthenticationContext context,
CancellationToken cancellationToken) { return Task.FromResult(0); } public Task ChallengeAsync(HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken) { context.Result = new AADChallengeResult { Inner = context.Result }; return Task.FromResult(0); } public bool AllowMultiple { get { return true; } } private class AADChallengeResult : IHttpActionResult { public IHttpActionResult Inner { get; set; } public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { HttpResponseMessage response = await Inner.ExecuteAsync(cancellationToken); if (response.StatusCode == HttpStatusCode.Unauthorized) { string challengeMessage = string.Format("authorization_uri=\"https://login.windows.net/{0}\",resource_id={1}", ConfigurationManager.AppSettings["ida:Tenant"], ConfigurationManager.AppSettings["ida:Audience"]); AuthenticationHeaderValue authenticateHeader =
new AuthenticationHeaderValue("Bearer", challengeMessage); response.Headers.WwwAuthenticate.Add(authenticateHeader); } return response; } } }
As you can imagine, the core of the challenge generation logic takes place in AADChallengeResult and specifically in ExecuteAsync. In summary: if the response is carrying a 401, the code inserts in the www-authenticate header a “Bearer” followed by a comma separated values list containing authorization_uri (the authority the Web API wants you to use when requesting a token for it) and resource_id (the ID you should sue with that authority when requesting a token for this Web API).
Both values come straight from the AppSettings entries populated by the template at project creation time. That means that the code you see here is 100% boilerplate: you can paste it in ANY Web API project generated with VS2013 with organizational account authentication, and as long as you didn’t change anything fundamental it will work right away as is with no changes need.
Using AuthenticationParameters in the Client’s Code
Moving back to the client. You can use any .NET client type you like; here I’m using a WinForm app given that it’s super fast. Follow the instructions in the section “Register a Native Client” in this article to let WIndows Azure AD know about your client and enable the necessary permissions for the client to call your Web API. That done, add the following code in the click handler of a button:
1: private async void button1_Click(object sender, EventArgs e)
2: {
3: string webApiUrl = "https://localhost:44305/api/Values";
4: string clientID = "220c8fe6-5f65-405f-bff3-089e88a2b9d4";
5: string returnUri = "http://challengedclient";
6:
7: AuthenticationParameters ap =
8: AuthenticationParameters.CreateFromResourceUrl(new Uri(webApiUrl));
9: AuthenticationContext ac =
10: new AuthenticationContext(ap.Authority);
11: AuthenticationResult ar =
12: ac.AcquireToken(ap.Resource,
13: clientID,
14: new Uri(returnUri));
15:
16: HttpClient client = new HttpClient();
17: HttpRequestMessage request =
18: new HttpRequestMessage(HttpMethod.Get, webApiUrl);
19: request.Headers.TryAddWithoutValidation("Authorization",
20: ar.CreateAuthorizationHeader());
21: HttpResponseMessage response =
22: await client.SendAsync(request);
23: string responseString =
24: await response.Content.ReadAsStringAsync();
25:
26: MessageBox.Show(responseString);
27: }
Now, this is the part I want to drill in a bit.
Lines 3, 4 and 5 hold the initial values we know of from the client’s perspective: the client coordinates, which will remain the same (usually) no matter which resource we’ll access, and the physical, network-addressable endpoint at which the target resource resides.
Lines 7 is where the magic happens. The class AuthenticationParameters is especially designed to help you to take advantage of resources that can emit challenges: it contains logic for eliciting a challenge and make the results easily accessible to you. What is demonstrated here is the case in which you know for sure that the resource supports challenges and you want to take advantage of that upfront: however this is not the only way to use it, as we’ll see later.What is demonstrated here is the case in which you know for sure that the resource supports challenges and you want to take advantage of that upfront: however this is not the only way to use it, as we’ll see later.
The static method CreateFromResourceUrl simply GETs the resource at the URL provided, and wraps the salient elements of the challenge in the properties of a new AuthenticationParameters instance.
Line 9 demonstrates how to use the information received from the challenge to create your AuthenticationContext against the AAD tenant requested by the resource.
Line 11 shows how to use the resource ID obtained form the challenge to acquire a token correctly scoped.
The rest of the code is just the usual call to a default Web API, which you have seen a million times by now
IMPORTANT: using the challenge information to drive authentication entails a certain amount of risk. For what concerns authenticating the user this is somewhat mitigated by the authority validation in the AAD case, though it remains a risk for the ADFS case (where the automatic validation feature is not available and AuthenticationContext calls must turn that off). You should ensure you perform whatever checks make sense in your scenario before blindly trusting what you receive in the challenge.
Special consideration should be given to the risk of token forwarding attacks. Imagine that a malicious resource A wants to gain access to resource B. If you use the challenge mechanism for calling A, A might send you a challenge containing the resource ID for B. If you’d just blindly follow what A says, you’d obtain a token scoped for B and present it in your call for A. At that point, A could use that token for calling B and pretending to be you. NOT good.
How do you defend yourself from this? Mostly by enforcing constraints on the shape of the resource IDs received. For example, you might want to impose that the URI of the resource returned in the challenge uses the same domain/hostname of the physical address at which the resource is listening from. That is not always applicable, especially at development time or if you didn’t register a vanity domain; but it’s a good example of what you might want to do to prevent issues.
And of course, all the basic networking hygiene applies here: do this only on HTTPS, ensure that the SSL channels are correctly validated to prevent DNS attacks, and so on.
The Solution in Action
Let’s give the solution a spin! Start both the Web API and the client project, put a breakpoint on line 9 on the client and hit the button.
The challenge generation logic worked, and AuthenticationParameters contains the right values. If you hit F5 you’ll see that the call performed according to those parameters succeeds. Q.E.D.
Want More?
This is a very basic use of the feature. As you have seen in the class diagram excerpt above, AuthenticationParameters offers other methods for populating its properties. Those are meant for more reactive scenarios, in which you receive a 401 during an actual call to the Web API and the response contains a challenge. The two overloads are meant to offer you the convenience of working at whatever level (header or entire response) you are confortable with, or what is dictated by the error management in the client library you are using if the resource you are targeting offers one.
You’d want to modify that logic for Web API working against ADFS (you would not hardcode login.windows net in the authority_uri). The warnings I gave earlier are especially serious when working with an on-premises instance, hence I recommend you do as much due diligence as possible before enabling the feature.
The static logic shown here would not work in case of multi-tenant resources: in that case you’d likely add some route-based logic in your filters to customize the challenge to the AAD tenant meant to secure a given Web API route (which might contains hints of the tenant it is meant for). Remember: for URLs that carry no indication of who the intended tenant might be, there’s always the option using the Common endpoint
Finally: the use of this feature might at times lead to some odd behaviors if the Web API does not do its job properly. Say that the challenge generation logic is buggy and the requirements sent back in the challenge are not the same requirements enforced at request time: if your client is using the challenge to react to access errors, it might end up in a poisoned loop in which it obtains a token which complies to the Web API challenge – but that it fails when used, generating the same challenge and going back to square 1. There’s no automated logic in ADAL to help you detecting this situation, given that ADAL stays out of your way when you are actually accessing the resource, hence to prevent similar situations you’ll have to make the necessary checks in your own code.
That said, I don’t want the above to scare you! like everything concerning security this feature requires due diligence, but once that’s done you get clients that can be self-configuring without the need of out of band settings acquisition logic. I believe this will be super important, especially once this capability will be available for the ADAL flavors targeting applications stores