If anyone sees any gaping holes in this scheme, or has a more elegant solution to the HTTP-for-anonymous/HTTPS-for-logged-in-users pattern, I'd love to hear the critique.
As a commenter mentioned above, the weak point here is the fact that you only enforce HTTPS after the user has logged in. Since your login page is served insecurely, an active attacker could modify it to steal passwords. A well known tool to do this is SSLStrip: http://www.thoughtcrime.org/software/sslstrip/
Thanks for the example. Looks like we'll give up attempting to serve any HTTP pages in the future, and do as you recommend -- I don't see any way of getting around the login-form-phishing hack you describe.
Well here's one way I think would work, but it's not ideal and still not 100% foolproof:
A) Keep a running counter between the server and client.
B) If, at any point, there are two active sessions both associated with the same user login, delete both sessions (thus logging out both the legitimate user and the attacker).
Session hijacking is still possible with this method, but only for the duration of one request (as long as you, the legitimate user, remember to log out at the end).
The main disadvantage is that if the request or response gets messed up, the session will be lost. Even if someone does something as simple as click a link twice before the response comes back, the session will be lost. So, yea, kind of a major usability drag...
Although... you could probably eliminate most lost session situations by using the counter method and allowing some leeway in it (say 2 or 3 requests off) to take into account double link click scenarios. That might be a good compromise...
Why don't you just run the whole site under SSL? That and add the STS header as an easy fix for people messing with the HTTP redirect to SSL. Might be a minuscule amount of extra load for your servers, but it has not degraded my Facebook experience noticeably (I have SSL enabled).