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):
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:
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