Azure-functions-host: Authenticated POST requests with well-known User-Agent string are rejected (403)

Created on 19 Jun 2017  路  9Comments  路  Source: Azure/azure-functions-host

Authenticated (with "AppServiceAuthSession" cookie set) POST requests with User-Agent header looking similar to the real browser user-agent are rejected. Requests with fake User-Agent header are processed successfully.

Repro steps

  1. Set up a new Azure Function App in Azure portal.

  2. Enable Authentication / Authorization with Azure Active Directory; disallow unauthenticated requests.

  3. Deploy demo function from https://github.com/penartur/MinimalFunctionAppDemo/blob/authentication-useragent/FunctionApp/PostFunction.cs

  4. Configure and run demo console app from https://github.com/penartur/MinimalFunctionAppDemo/blob/authentication-useragent/ConsoleApp/Program.cs

Expected behavior

Sending request for MyTestApp
OK: "Read 1000 bytes"
Sending request for Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0
OK: "Read 1000 bytes"
Sending request for Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
OK: "Read 1000 bytes"
Sending request for Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240
OK: "Read 1000 bytes"
Sending request for Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; InfoPath.3; rv:11.0) like Gecko
OK: "Read 1000 bytes"
Sending request for MyTestApp/5.0 (MyTestOS 10.0; WOW64; rv:53.0) MyTestEngine/20100101 MyTestApp/53.0
OK: "Read 1000 bytes"

Actual behavior

Sending request for MyTestApp
OK: "Read 1000 bytes"
Sending request for Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0
Forbidden: You do not have permission to view this directory or page.
Sending request for Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
Forbidden: You do not have permission to view this directory or page.
Sending request for Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240
Forbidden: You do not have permission to view this directory or page.
Sending request for Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; InfoPath.3; rv:11.0) like Gecko
Forbidden: You do not have permission to view this directory or page.
Sending request for MyTestApp/5.0 (MyTestOS 10.0; WOW64; rv:53.0) MyTestEngine/20100101 MyTestApp/53.0
OK: "Read 1000 bytes"

Known workarounds

Modifying user-agent allows one to send authenticated POST requests. However, this is not possible to do in all browsers.

Most helpful comment

Weird, it does not seem to affect the login flow for me.

Ah, sorry for any confusion. In this particular redirect scenario, we require the redirect domain _and path_ to match exactly. In your case, you specified https://www.frontend.org in the list of allowed locations but your query string parameter specified https://www.frontend.org/path. If we don't see a valid match, we log a warning in your application logs and redirect you to the default "login completed" page. The warning will look like the following in streaming logs:

2017-06-19T23:48:07 PID[2012] Warning The requested location 'https://www.frontend.org/path/' was not found in the list of allowed external redirect URLs.

If you instead specify the full URL in allowed external redirect URLs (https://www.frontend.org/path/), then the redirect will work as you might expect. The path portion of the URL is not required to match exactly in the CSRF case, however, which makes the behavior a bit more unexpected.

This is off topic from the original issue, but a good thing to clarify anyways.

All 9 comments

@cgillum This sound like some app service authentication behavior.. is this expected?

@penartur, thanks for the detailed report! If you turn on Application Logging in the portal (under platform features) you should see that the Authentication / Authorization module is rejecting these calls as part of our built-in CSRF mitigation. Several conditions are required for you to run into this:

  1. It's a POST request that authenticated using a session cookie
  2. The request came from a known browser (as determined by the user agent)
  3. The HTTP Origin or HTTP Referer header is missing or is not in the configured list of approved external domains for redirection.
  4. The HTTP Origin is missing or is not in the configured list of CORS origins.

Looking at your client code, it seems all of these are true.

Your setup seems a little unnatural to me - i.e. a console app would ideally never authenticate using cookies and a real browser would always enable CORS before sending cross-domain requests. Is this a blocking issue for you or can you work around it using one of the implied workarounds listed above?

Your setup seems a little unnatural to me
Is this a blocking issue for you or can you work around it using one of the implied workarounds listed above?

Considering #620, my setup is quite natural. Yes, this is a blocking issue for me.

I tried to create a web application as a combination of static HTML+JS frontend hosted elsewhere (e.g. Github Pages) and Azure Functions, authenticating users with Azure authentication. This is quite an obvious use case of Functions IMHO.
As Azure does not return Access-Control-Allow-Credentials header even with enabled CORS, I had to return all CORS headers manually in my Functions, and to clear the list of allowed origins in Azure portal so that Azure won't overwrite my explicit CORS headers. Without that, my frontend couldn't even process GET requests to Functions, as it did not send credential cookie to server.
But with this workaround for #620, POST requests are broken, because you seem to believe that HTTP Origin is not in the configured list of CORS origins (because said list is empty in Azure portal).

Right now, combination of #620 and #1602 makes the obvious use case of static frontend + Azure Functions backend with Azure authentication impossible to implement.

I see, so you're trying to workaround other bugs in the system and then running into this.

But there's certainly nothing impossible about your scenario. I can immediately think of two workarounds:

  1. Add your client domain to the list of allowed external redirect domains. You can find this list under Platform Features --> Authentication / Authorization --> Allowed External Redirect Urls).
  2. Use session tokens instead of cookies to authenticate with your functions. The easiest way is to use the App Service JavaScript Client SDK to do the login and function calls - it works with functions and will use token auth under the covers.

Can you first try (1) and see if it resolves your issue? Just add the https://{domain} base address of your static HTML+JS frontend to this list.

Can you first try (1) and see if it resolves your issue? Just add the https://{domain} base address of your static HTML+JS frontend to this list.

Yes, it does resolve the original issue with my application; POST requests are now processed successfully in browser. Unfortunately, the documentation is not very extensive on this setting, so I'm not sure what else it does.

Glad this unblocked you - thanks for confirming. I'll go ahead and close this issue.

Just for background, this setting is primarily intended for scenarios where you complete a login flow and use the post_login_redirect_uri={url} query string parameter to redirect the user to a custom location. We don't allow redirecting to arbitrary domains because we don't want attackers to be able to steal tokens (which may be embedded in the redirect location) by tricking you into clicking malicious links. This setting allows you to specify which locations are safe for redirection.

Because URLs added to this list are considered "trusted", our CSRF protection logic also considers this list when deciding whether to allow cookie-authenticated POST requests.

Just for background, this setting is primarily intended for scenarios where you complete a login flow and use the post_login_redirect_uri={url} query string parameter to redirect the user to a custom location.

Weird, it does not seem to affect the login flow for me. When I redirect user to https://myfunctionapp.azurewebsites.net/.auth/login/aad?post_login_redirect_url=https%3A%2F%2Fwww.frontend.org%2Fpath%2F, user just gets "You have successfully signed in" page, with "return to the website" link redirecting user to https://myfunctionapp.azurewebsites.net/. Adding https://www.frontend.org/ to the list of external redirect domains, while fixing POST requests problem, did not change anything with login process. When I set relative URL as post_login_redirect_url, everything works fine (but the user, obviously, ends up on the https://myfunctionapp.azurewebsites.net/relativeurl).

I understand that this is outside of Azure Functions scope, but could you please direct me to some relevant documentation so that I could make this scenario work? (You seem to imply that it should work)

And just to make sure, and to outline the full workaround to other developers, in static frontend + Azure Functions + Azure authentication scenario (without using azure client SDK), to make everything work, one should:
1) Enable CORS and delete all domains from the list;
2) Explicitly return appropriate Access-Control-Allow-Origin (matching Origin request header, if it is allowed) and Access-Control-Allow-Credentials (set to true) headers from their function;
3) Add their frontend URL to the Allowed External Redirect Urls under Authentication.

Weird, it does not seem to affect the login flow for me.

Ah, sorry for any confusion. In this particular redirect scenario, we require the redirect domain _and path_ to match exactly. In your case, you specified https://www.frontend.org in the list of allowed locations but your query string parameter specified https://www.frontend.org/path. If we don't see a valid match, we log a warning in your application logs and redirect you to the default "login completed" page. The warning will look like the following in streaming logs:

2017-06-19T23:48:07 PID[2012] Warning The requested location 'https://www.frontend.org/path/' was not found in the list of allowed external redirect URLs.

If you instead specify the full URL in allowed external redirect URLs (https://www.frontend.org/path/), then the redirect will work as you might expect. The path portion of the URL is not required to match exactly in the CSRF case, however, which makes the behavior a bit more unexpected.

This is off topic from the original issue, but a good thing to clarify anyways.

Was this page helpful?
0 / 5 - 0 ratings