ember-simple-auth
ember-simple-auth copied to clipboard
using HttpOnly cookies
I was thinking about improving security when making use of the cookie store and was wondering if we could make use of HttpOnly cookies when using ember-simple-auth, and it seems that it actually is possible.
But to provide a little background first, this is what the owasp session management cheatsheet contains about httpOnly cookies:
The HttpOnly cookie attribute instructs web browsers not to allow scripts (e.g. JavaScript or VBscript) an ability to access the cookies via the DOM document.cookie object. This session ID protection is mandatory to prevent session ID stealing through XSS attacks.
Ultimately this means that our app will not be able to access any information regarding if we actually have a valid session token or not, which comes with some implications (which I think all can be worked around one way or another - at very least by placing an additional cookie (or local storage) that does not contain any sensitive information):
- synchronisation across multiple browser tabs will not work as we can not query if there is a cookie containing our token
- we will need to issue a request to actually find out if we have a valid session or not
The HttpOnly cookie will be included within every request we make to the backend, so everything should still work as it did before, as long as the backend accepts the token via the cookie header.
As mentioned, both issues can be worked around by storing non sensitive information (like the expiration date) in a cookie (or other means of storage) that is accessible from javascript. Regarding the latter point, it is also possible to leverage Fastboot to take care of that, as we can make requests to the backend from our fastboot server before the initial page is served to the client and on the browser is able to check if that data is contained within the fastboot shoebox. On the client side we either have a session/user in our shoebox or we are not able to authenticated and don't need to bother to check. However, this places fastboot within a somewhat critical path, as when fastboot for whatever reason breaks, a user coming back will have to log in again.
There are a few little things that feel a little weird when currently using ember-simple-auth with this approach (like having to return a data object in restore which not actually contains any useful information).
I am opening this issue mostly to see if there is any interest in incorporating this pattern with ember-simple-auth and I am pretty much interested what you all think about this approach.
Using HttpOnly cookies for auth is actually a pretty good approach and something you can already do with ESA. Most of the apps using ESA I think are using the OAuth 2.0 flow where the cookie is sent in the response to the authentication request which necessarily means it ends up in the client app and could potentially be accessed by an attacker in case of XSS vulnerabilities (in theory even if it wasn't stored in ESA's cookie or localStorage key).
What you can do though is:
- instead of using the OAuth 2.0 flow that responds with the token in a cookie, use an API (and matching ESA authenticator) that responds with status 200 and an empty response body and also an HttpOnly cookie that contains the token
- the ESA authenticator can tell from the 200 response code that authentication was successful although it cannot actually see the token
- ESA will still set its own non-HttpOnly cookie that contains its internal state so that the session survives a reload etc. That cookie will not contain any sensitive information anymore though as there is no sensitive information in the session itself anymore; it will only contain the fact that the session is authenticated and the authenticator in use.
- since the HttpOnly cookie will automatically be included in all requests to the issuing server, requests will actually be authorized; once the cookie expires, the server will respond with a 401 status which ESA will see and automatically invalidate the session
I think there are no changes necessary in ESA but this is mostly a documentation issue. If you'd like to submit a PR adding a guide for this pattern that would be a great addition I think!
// authenticators/zap-api-authenticator.js
export default class ZAPAuthenticator extends BaseAuthenticator {
authenticate() {
const { access_token } = _parseResponse(location().hash);
// returns a user object and http cookie for later requests
return fetch(`${ENV.host}/login`, {
headers: {
Authentication: `Bearer ${access_token}`,
},
});
}
restore() {
// authentication is implicit with an HttpOnly cookie
// this request will fail if that cookie doesn't exist
// user "findAll" as #find requires an id...
return this.store.findAll('user');
}
}
Thanks for that thorough explanation @marcoow — I went ahead and just extended BaseAuthenticator.
The only tricky-ish part is dealing with overriding the restore method. I have it hitting some users endpoint, which, if authenticated by the presence of an HttpOnly cookie, will return another user object. I'm okay with that, but it would be nice if I stash some identifier information in that ESA non-HttpOnly cookie. That way I would have a more RESTful-looking call to /users.
It's a tiny nitpick but wanted to share out of interest.
@allthesignals I think I am doing something quite similar. However, in recovery I actually handle what is published here to get the current user. So, if restore succeeds, it loads the current user and makes it available to the app, if it fails the server invalidates the cookie containing the token. The login api also returns the current user when it succeeds, so I do not load the current user anywhere else, which I found quite convenient.
In reality things are a little more complex, as it involves fastboot, which allows to actually distinguish if we have a http only cookie or not (something you can not do in the browser).
Great that this issue has become a place to discuss this topic.
@marcoow I think you are right about this only being a matter of documentation. What I personally did not like much about the cookie store implementation is that it tends to put all the things that are available as session into the cookie (unless I missed something), which then gets sent to the server on each request while usually we only need a small subset of that data on the server.
Thanks @st-h — looks like queryRecord makes a lot more sense here:
In this example, the service does not need to know the ID of the current user as it uses a dedicated endpoint instead that will always respond with the user belonging to the authorization token in the request.
@marcoow I think you are right about this only being a matter of documentation. What I personally did not like much about the cookie store implementation is that it tends to put all the things that are available as session into the cookie (unless I missed something), which then gets sent to the server on each request while usually we only need a small subset of that data on the server.
I think this is a matter of documentation as well actually. The session is not supposed to contain more than a few atomic values. Returning a full user object means a JSON serialization of that object ends up in the session store which you would not be able to restore an Ember Data model from anyway when the application restarts and that string is read from the cookie. In this case, when you don't have any data to store in the session (as the only thing that'd be directly relevant to the session wich is the token is stored in the cookie which is invisible to JS anyway), it'd be totally fine to resolve with an empty object from the authenticated method and just always resolve from restore as well (as you cannot know based only on the session data whether the token the user has is still valid and thus the session still authenticated; if the token is actually not valid anymore, the session would be invalidated automatically upon the next request made to the API anyway):
export default class ZAPAuthenticator extends BaseAuthenticator {
async authenticate() {
const { access_token } = _parseResponse(location().hash);
// returns a user object and http cookie for later requests
await fetch(`${ENV.host}/login`, {
headers: {
Authentication: `Bearer ${access_token}`,
},
});
return {}; // we have nothing to store in the session
}
restore(data) {
return Promise.resolve(data); // we cannot know whether the session is still authenticated as we cannot see the token anyway;
}
}
You can handle the current user (outside of the authenticator) following the guide.
HttpOnly is for the backend, you can't setup by just running the JavaScript code. You would need your nodejs to do all the trick