As described in my previous post, I've just finished building some Silverlight clients and a portal for the new OData service for SQL Azure using AppFabric Access Control (ACS). In the process, I ran into a few issues worth documenting.
Before we get to harder problems, let’s make sure we have the basics right.
If you’re doing Silverlight client development, then you probably already know that any service accessed by a Silverlight client must either be hosted from the same Internet zone, domain and scheme as the client, or have a cross domain security policy file in place. What you may not know is that the AppFabric Team recently released a version of ACS to AppFabric Labs with a cross domain security policy file, allowing access from any domain over either http or https to any path below the base URI of any STS endpoint. You should be able to obtain and inspect the file using your web browser.
You probably also know that you need to specify client HTTP handling for REST endpoints like ACS. What you may not know is that for cross domain access to data using WCF Data Services (formerly ADO.NET Data Services), you’ll either need to move to Silverlight 4, or use ADO.NET Data Services for Silverlight 3 Update CTP3 (thanks to Mike Flasko and Pablo Castro for that pointer).
Now, assuming you’re using the right networking stack, and hitting the AppFabric Labs release of ACS, instead of production, you may see Not Found (404) WebExceptions raised when you try to request a token.
These exceptions are often misleading. If you read the response body, you may find that the problem is reported as an XML formatted “request unauthorized” error in some cases, or as an HTML formatted “HTTP method not allowed” in other cases.
The “request unauthorized” error is expected behavior from ACS when the credentials needed to authenticate with the service are missing. The only surprise here is that it is reported by the Silverlight as Not Found (404), instead of the expected Unauthorized (401).
The “HTTP method not allowed” error is more perplexing. When you see it, you may also notice that the web request identified in the exception is not the one you used to do the POST to request the token, but rather a GET with different headers targeting the same URI.
The Case of the Missing Slash
The “HTTP method not allowed” error can be hard to track down, since it occurs inside the HTTPS tunnel, where it can’t be directly observed. Aaron Oneal provided the critical clue: when a POST results in a redirect, with auto-redirect enabled, Silverlight strips the request headers and issues a GET to the redirect address, not a POST. User code in the client sees one operation, while the server sees two.
The clue was enough for Cyrus Harvesf to understand the problem. When the endpoint address used to access ACS doesn’t contain a trailing slash, ACS redirects the caller to the same address with a trailing slash. When the Silverlight networking stack responds with a GET to the redirect address, the GET operation fails because it is not supported by the endpoint.
Why didn’t we notice this problem before?
Because the .NET framework silently replays POSTs in response to redirects, so omitting the trailing slash didn’t block any scenarios until now, with the enablement of Silverlight access in AppFabric Labs. Even more insidious is that in the case of POSTs to ACS without the trailing slash, the redirect and silent replay take place inside the HTTPS tunnel, where no one can really see what’s going on, even with any of the popular network monitoring tools (e.g., NetMon, WireShark, Fiddler, etc.).
Which behavior is correct?
Marco Matos notes that the Silverlight behavior is designed to prevent cross site attacks in which request headers on a POST could be redirected to unknown domains. Clearly, this is preferable to the .NET framework behavior, which puts the client at risk. However, the HTTP specification says that in the case of a 301 or 303 response code, the user agent should not redirect the request without explicit permission from the user, unless the request is either a GET or HEAD, for precisely this reason. So, according to the specification, neither behavior is correct.
Down the Garden Path
Now, here’s the kicker. Most of us copy and paste generated URIs to make sure nothing is lost in translation. However, the AppFabric portal displays the addresses for the endpoints it provisions WITHOUT the trailing slashes required by ACS, thereby ensuring that most of us have it wrong. Quick… go add slashes to all your STS endpoint addresses.
How should ACS correct the problem?
It could fail outright when the trailing slash is missing, instead of redirecting the client. However, changing the behavior now would break existing clients, if they are using URIs without the trailing slashes.
A better approach would be to preserve the existing behavior in the next release, and to document the problem, with a warning that the behavior may change in later releases. This would give clients time to prepare for the change, by making sure their URIs have trailing slashes.
So, now we’re ready to rock and roll, right? Not quite.
There’s an important requirement in OAuth WRAP that can’t be satisfied using Silverlight 3 and earlier versions. The specification recommends that that clients place the SWT in the authorization header:
Clients always present an Access Token to access a Protected Resource. Use of the Authorization header is RECOMMENDED, since HTTP implementations are aware that Authorization headers have special security properties and may require special treatment in caches and logs. Protected Resources SHOULD take precautions to insure that Access Tokens are not inadvertently logged or captured. In addition to the methods presented here, the Protected Resource MAY allow the Client to present the Access Token using any scheme agreed on by the Client and Protected Resource.
Unfortunately, the Authorization header can’t be set in versions prior to Silverlight 4 because it’s on the list of restricted headers, and it can only be set in Silverlight 4 when the cross domain security policy file contains an appropriate entry (e.g., an allow-from element with an http-request-headers attribute whose value contains the name of the Authorization header).
The best solution is to move to Silverlight 4. However, for clients that can’t make the move, an alternate header should be a workable solution, provided that the service supports it, and configures the cross domain security policy file to allow it, as described above.
For example, the OData Service checks both Authorization and X-Authorization. If it finds tokens in both locations, they must be identical. Also, the ACS management service checks Authorization and x-ms-Authorization. If it finds tokens in both locations, it returns an error to the client.