A number of similar questions have been asked about this general topic before (e.g. How to store user credentials in browser securely?), but I wanted to seek some clarification on a specific scenario.
I have what is essentially a stateless web application (it's a webmail client). As such, when a user logs in, the application isn't actually able to authenticate the user directly, it just passes the credentials to another server (an interaction in which the application is acting as the client). Essentially, this application is functioning as a sort of intermediary. For clarity, let's call the client
C, the server
S, and the other, remote server as
R. Note that
R may be a third-party server, i.e. there is no way to "cooperate" between
R. However, we can assume
R is trusted.
Currently, authentication support is fairly basic; it's regular username/password only. Currently, there are server-side sessions, and the server stores the client's credentials info in plaintext for the duration of the session. I would like to accomplish two things:
- Avoid the use of server-side sessions altogether.
- Prevent the server from ever seeing the user's password altogether. If challenge/response authentication is being used, I see no reason that
Sshould be concerned with. Right now, I'm careful in that the password is never logged, but it's still stored on disk during the session, and I really don't like that.
S, but shouldn't need to disclose its password to it.
It would still like to continue to allow for persistent sessions (in the lose sense of the term).
A point of clarification about #2 though:
S should not ever be able to learn the password, but it could certainly be allowed to invalidate the client's knowledge of it, provided that #1 still holds (more on this at the bottom).
For number 1, I imagine using something like a JWT instead of session cookies would take care of this. This way,
S wouldn't need to keep track of anything, and for this particular application, this would actually make sense.
For number 2, let's assume that
R supports a challenge/response authentication mechanism. What I'm envisioning is that
C can open a connection to
S, but rather than sending credentials in the initial request,
S can first connect to
R and (assuming challenge/response is supported), relay the challenge from
R back to
C will know the plaintext password, and can response to the challenge to
S, which will relay the answer to
R, completing authentication.
This would accomplish goals #1 and #2, but circling back to goal #3, I'd still like for the user to be able to refresh the application without having to reauthenticate, which means the client, presumably, would need to store the password locally. I thought about perhaps encrypting it somehow and maybe storing it in a JWT, but this wouldn't really accomplishing anything.
Since the client will need the plaintext password for challenge/response, and in order to satisfy goal #2, the server can't store the password anymore, or even encrypt it in such a form that the client could later ask it to decrypt it, since it would still know it at some point, even if it's not storing it. So I think this would need to be fully client-side.
HttpOnly, which would open it up to possible XSS attacks. The application should be pretty robust against these, and has a good Content Security Policy, but this might not be 100% foolproof. It might be safe, and not any worse than local storage, but maybe not ideal. And since we don't actually want to send the data to the server, a cookie is really inappropriate anyways (it might be a good solution for a JWT, if #1 was the only requirement, but due to #2, I don't think it would work).
I did stumble upon this authentication API that most modern browsers seem to support: https://developer.mozilla.org/en-US/docs/Web/API/Credential_Management_API, also raised in this question: https://stackoverflow.com/questions/72127461/how-to-correctly-store-user-secrets-in-a-frontend-application
I'm not concerned about mobile support, but backwards-compatibility on desktop browsers is important.
One problem with that approach is that's entrusting the storage of passwords to the operating system, which is generally better, particularly if the user's machine is trusted, but there may be cases where this wouldn't be as ideal (e.g. logging into the application on a public or shared machine). Private/incognito mode would be sufficient now, since session cookies are deleted when the browser is closed, but this wouldn't have a nice equivalent here, from what I can see.
Then again, a user could simply opt to not remember a password in this case that it's not his/her machine, but would still need to log in every time the page is refreshed or opened again, since the password wouldn't be stored at all. But this is an edge case that's not super important.
Is there any mechanism that would allow for simultaneously being able to:
Sfrom ever knowing the password
- Retain the ability to "delete" all client-side knowledge of the password if, the application is being used in a private window, simply closing the browser
- Not introduce any possible cross-site access to the password
- Allow for expiration of the password at a certain point in time.
It seems like the viable options are either local storage or using the authentication API. Cookies seem to be the only way to satisfy #4 (to some extent, the client can obviously modify the expiration), but they'd be inappropriate due to #1. Both of these are incompatible with #4, so maybe Remember Me would just need to be limited to "Never" or "Forever", but just wondering if there are any other approaches that I've missed here? There just seem to be tradeoffs regardless of the approach taken.
Alternately, would there be an optimal way to combine certain techniques? For example, maybe the client could encrypt the password, store the encrypted password locally, send the encryption key to the server, and then receive a JWT in response as an HttpOnly cookie, that could be used at a later point to retrieve the encryption key. At least my cursory analysis suggests this would be safe, since neither side alone has all the information required (except the client, during authentication) to reconstruct the plaintext password. And when the JWT expires (independent of the cookie expiring), then the client wouldn't be able to reconstruct the password anymore and the user would need to log in again, which could be a nice property. Each time the page loads, if the JWT is still valid, the client could receive the decryption key in response and use that to temporarily decrypt the password for as long as is needed to answer the challenge/response, and then, ideally, it would be purged from memory.
We can assume
S won't be compromised, or at least certainly that the case of somebody stealing the encryption key from
S and then going to
C to decrypt the password is not a concern.
Would this be a good approach, and are there even better alternatives?