3

I'm experiencing my client getting logged out after an innocent request to my server. I control both ends and after a lot of debugging, I've found out that the following happens:

  • The client sends the request with a correct Authorization header.
  • The server responds with 304 Not Modified without any Authorization header.
  • The browser serves the full response including an obsolete Authorization header as found in its cache.
  • From now on, the client uses the obsolete Authorization and gets kicked out.

From what I know, the browser must not cache any request containing Authorization. Nonetheless,

chrome://view-http-cache/http://localhost:10080/api/SearchHost

shows

HTTP/1.1 200 OK
Date: Thu, 23 Nov 2017 23:50:16 GMT
Vary: origin, accept-encoding, authorization, x-role
Cache-Control: must-revalidate
Server: 171123_073418-d8d7cb0 =
x-delay-seconds: 3
Authorization: Wl6pPirDLQqWqYv
Expires: Thu, 01 Jan 1970 00:00:00 GMT
ETag: "zUxy1pv3CQ3IYTFlBg3Z3vYovg3zSw2L"
Content-Encoding: gzip
Content-Type: application/json;charset=utf-8
Content-Length: 255

The funny server header replaces the Jetty server header (which shouldn't be served for security reasons) by some internal information - ignore that. This is what curl says:

< HTTP/1.1 304 Not Modified
< Date: Thu, 23 Nov 2017 23:58:18 GMT
< Vary: origin, accept-encoding, authorization, x-role
< Cache-Control: must-revalidate
< Server: 171123_073418-d8d7cb0 =
< ETag: "zUxy1pv3CQ3IYTFlBg3Z3vYovg3zSw2L"
< x-delay-seconds: 3
< Content-Encoding: gzip

This happens in Firefox, too, although I can't reproduce it at the moment. The RFC continues, and it looks like the answer linked above is not exact:

unless a cache directive that allows such responses to be stored is present in the response

It looks like the response is cacheable. That's fine, I do want the content to be cached, but I don't want the Authorization header to be served from cache. Is this possible?

Explanation of my problem

My server used to send the Authorization header only when responding to a login request. This used to work fine, problems come with new requirements.

Our site allows users to stay logged in arbitrarily long (we do no sensitive business). We're changing the format of the authorization token and we don't want to force all users to log in again because of this. Therefore, I made the server to send the updated authorization token whenever it sees an obsolete but valid one. So now any response may contain an authorization token, but most of them do not.

The browser cache combining the still valid response with an obsolete authorization token comes in the way.

As a workaround, I made the server send no etag when an authorization token is present. It works, but I'd prefer some cleaner solution.

maaartinus
  • 44,714
  • 32
  • 161
  • 320
  • If the linked answer led you to believe that the browser must not cache anything containing Authorization, then it is misleading. It is actually the opposite, which means that Chrome's cache does its job by the book. I suspect you've figured this out already. You just need more information to devise a solution, right? – Rei Dec 01 '17 at 21:29
  • @Rei Right, I was confused by the linked answer. I'm also confused by the RFC as such a behavior makes little sense to me. For the problem, see the question update. – maaartinus Dec 01 '17 at 22:14

2 Answers2

6

The quote in the linked answer is misleading because it omitted an important part: "if the cache is shared". Here's the correct quote (RFC7234 Section 3):

A cache MUST NOT store a response to any request, unless: ... the Authorization header field (see Section 4.2 of [RFC7235]) does not appear in the request, if the cache is shared,

That part of the RFC is basically a summary. This is the complete rule (RFC7234 Section 3.2) that says essentially the same thing:

A shared cache MUST NOT use a cached response to a request with an Authorization header field (Section 4.2 of [RFC7235]) to satisfy any subsequent request unless a cache directive that allows such responses to be stored is present in the response.

Is a browser cache a shared cache? This is explained in Introduction section of the RFC:

A private cache, in contrast, is dedicated to a single user; often, they are deployed as a component of a user agent.

That means a browser cache is private cache. It is not a shared cache, so the above rule does not apply, which means both Chrome and Firefox do their jobs correctly.

Now the solution.

The specification suggests the possibility of a cached response containing Authorization to be reused without the Authorization header. Unfortunately, it also says that the feature is not widely implemented.

So, the easiest and also the most future-proof solution I can think of is make sure that any response containing Authorization token isn't cached. For instance, whenever the server sees an obsolete but valid Authorization token, send a new valid one along with Cache-Control: no-store to disallow caching.

Also you must never send Cache-Control: must-revalidate with Authorization header because the must-revalidate directive actually allows the response to be cached, including by shared caches which can cause even more problems in the future.

... unless a cache directive that allows such responses to be stored is present in the response.

In this specification, the following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.

Community
  • 1
  • 1
Rei
  • 6,263
  • 14
  • 28
  • 1
    Your explanation is perfect, but I don't like the solution much. Without `must-revalidate`, the browser feels like it itself can decide if the response is valid and that's wrong. Neither I like disabling caching for requests containing the authorization header as the content should be cached; just the obsolete header should not. But I'm afraid HTTP doesn't support what I want, so I'll have to accept your answer. – maaartinus Dec 04 '17 at 09:22
  • @maaartinus I understand your disappointment. Then again, the specification was released in 2014, things could have improved. You may want to try `Cache-Control: no-cache=authorization`. – Rei Dec 04 '17 at 22:38
0

My current solution is to send an authorization header in every response; using a placeholder value of - when no authorization is wanted.

The placeholder value is obviously meaningless and the client knows it and happily ignores it.

This solution is ugly as it adds maybe 20 bytes to every response, but that's still better than occasionally having to resend a whole response content as with the approach mentioned in my question. Moreover, with HTTP/2 it'll be free.

maaartinus
  • 44,714
  • 32
  • 161
  • 320